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' vmImage: 'ubuntu-20.04'
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.423' displayName: 'Use .NET Core SDK 3.1.418'
inputs: inputs:
version: 3.1.423 version: 3.1.418
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401' displayName: 'Use .NET Core SDK 6.0.401'
@ -72,9 +72,9 @@ jobs:
vmImage: 'macos-12' vmImage: 'macos-12'
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.423' displayName: 'Use .NET Core SDK 3.1.418'
inputs: inputs:
version: 3.1.423 version: 3.1.418
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401' displayName: 'Use .NET Core SDK 6.0.401'
@ -155,9 +155,9 @@ jobs:
SolutionDir: '$(Build.SourcesDirectory)' SolutionDir: '$(Build.SourcesDirectory)'
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.423' displayName: 'Use .NET Core SDK 3.1.418'
inputs: inputs:
version: 3.1.423 version: 3.1.418
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401' displayName: 'Use .NET Core SDK 6.0.401'
@ -179,7 +179,7 @@ jobs:
displayName: 'Install Nuke' displayName: 'Install Nuke'
inputs: inputs:
script: | script: |
dotnet tool install --global Nuke.GlobalTool --version 6.2.1 dotnet tool install --global Nuke.GlobalTool --version 0.24.0
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Run Nuke' 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()] [CmdletBinding()]
Param( Param(
#[switch]$CustomParam,
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$BuildArguments [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 $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
########################################################################### ###########################################################################
@ -14,15 +15,15 @@ $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
########################################################################### ###########################################################################
$BuildProjectFile = "$PSScriptRoot\nukebuild\_build.csproj" $BuildProjectFile = "$PSScriptRoot\nukebuild\_build.csproj"
$TempDirectory = "$PSScriptRoot\\.nuke\temp" $TempDirectory = "$PSScriptRoot\\.tmp"
$DotNetGlobalFile = "$PSScriptRoot\\global.json" $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" $DotNetChannel = "Current"
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
$env:DOTNET_MULTILEVEL_LOOKUP = 0 $env:NUGET_XMLDOC_MODE = "skip"
########################################################################### ###########################################################################
# EXECUTION # EXECUTION
@ -33,37 +34,38 @@ function ExecSafe([scriptblock] $cmd) {
if ($LASTEXITCODE) { exit $LASTEXITCODE } 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 ` 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 $env:DOTNET_EXE = (Get-Command "dotnet").Path
} }
else { else {
$DotNetDirectory = "$TempDirectory\dotnet-win"
$env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
# Download install script # Download install script
$DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null mkdir -force $TempDirectory > $null
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
(New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) (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 # Install by channel or version
$DotNetDirectory = "$TempDirectory\dotnet-win"
if (!(Test-Path variable:DotNetVersion)) { if (!(Test-Path variable:DotNetVersion)) {
ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
} else { } 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 -- $BuildArguments }
ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }

60
build.sh

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

6
nukebuild/Build.cs

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

4
nukebuild/BuildParameters.cs

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

12
nukebuild/BuildTasksPatcher.cs

@ -17,8 +17,12 @@ public class BuildTasksPatcher
{ {
if (entry.Name == "Avalonia.Build.Tasks.dll") 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"; 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(); var patched = new MemoryStream();
try try
{ {
@ -57,10 +61,8 @@ public class BuildTasksPatcher
{ {
try try
{ {
if (File.Exists(temp)) if(Directory.Exists(tempDir))
File.Delete(temp); Directory.Delete(tempDir, true);
if (File.Exists(output))
File.Delete(output);
} }
catch catch
{ {

4
nukebuild/DotNetConfigHelper.cs

@ -46,7 +46,7 @@ public class DotNetConfigHelper
public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity) public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity)
{ {
Build = Build?.SetVerbosity(verbosity); Build = Build?.SetVerbosity(verbosity);
Pack = Pack?.SetVerbosity(verbosity); Pack = Pack?.SetVerbostiy(verbosity);
Test = Test?.SetVerbosity(verbosity); Test = Test?.SetVerbosity(verbosity);
return this; return this;
} }
@ -54,4 +54,4 @@ public class DotNetConfigHelper
public static implicit operator DotNetConfigHelper(DotNetBuildSettings s) => new DotNetConfigHelper(s); 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(DotNetPackSettings s) => new DotNetConfigHelper(s);
public static implicit operator DotNetConfigHelper(DotNetTestSettings 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"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
@ -7,11 +6,10 @@
<RootNamespace></RootNamespace> <RootNamespace></RootNamespace>
<IsPackable>False</IsPackable> <IsPackable>False</IsPackable>
<NoWarn>CS0649;CS0169</NoWarn> <NoWarn>CS0649;CS0169</NoWarn>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
</PropertyGroup> </PropertyGroup>
<Import Project="..\build\JetBrains.dotMemoryUnit.props" /> <Import Project="..\build\JetBrains.dotMemoryUnit.props" />
<ItemGroup> <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="xunit.runner.console" Version="2.3.1" />
<PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " /> <PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " />
<PackageReference Include="ILRepack.NETStandard" Version="2.0.4" /> <PackageReference Include="ILRepack.NETStandard" Version="2.0.4" />

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

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

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

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

2
src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs

@ -1,9 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Xml.Linq; using System.Xml.Linq;
using System.Linq; using System.Linq;

1
src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text; using System.Text;
using Avalonia.Markup.Xaml.PortableXaml; using Avalonia.Markup.Xaml.PortableXaml;
using Avalonia.Utilities; using Avalonia.Utilities;

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

@ -9,9 +9,6 @@
<DependentUpon>%(Filename)</DependentUpon> <DependentUpon>%(Filename)</DependentUpon>
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" /> <ProjectReference Include="..\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.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 class X11IconLoader : IPlatformIconLoader
{ {
private readonly X11Info _x11;
public X11IconLoader(X11Info x11)
{
_x11 = x11;
}
IWindowIconImpl LoadIcon(Bitmap bitmap) IWindowIconImpl LoadIcon(Bitmap bitmap)
{ {
var rv = new X11IconData(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<ICursorFactory>().ToConstant(new X11CursorFactory(Display))
.Bind<IClipboard>().ToConstant(new X11Clipboard(this)) .Bind<IClipboard>().ToConstant(new X11Clipboard(this))
.Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub()) .Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub())
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info)) .Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider()) .Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this)); .Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));

2
src/Shared/ModuleInitializer.cs

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

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

@ -30,6 +30,10 @@
<ItemGroup> <ItemGroup>
<WasmExtraFilesToDeploy Include="index.html" /> <WasmExtraFilesToDeploy Include="index.html" />
<WasmExtraFilesToDeploy Include="main.js" /> <WasmExtraFilesToDeploy Include="main.js" />
<WasmExtraFilesToDeploy Include="embed.js" />
<WasmExtraFilesToDeploy Include="favicon.ico" />
<WasmExtraFilesToDeploy Include="Logo.svg" />
<WasmExtraFilesToDeploy Include="app.css" />
</ItemGroup> </ItemGroup>
<Import Project="..\Avalonia.Web\Avalonia.Web.props" /> <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;
using Avalonia.Web; using Avalonia.Web;
using ControlCatalog; using ControlCatalog;
using ControlCatalog.Web;
internal partial class Program internal partial class Program
{ {
private static void Main(string[] args) 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"); 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="./main.js" />
<link rel="modulepreload" href="./dotnet.js" /> <link rel="modulepreload" href="./dotnet.js" />
<link rel="modulepreload" href="./avalonia.js" /> <link rel="modulepreload" href="./avalonia.js" />
<link rel="stylesheet" href="./app.css" />
</head> </head>
<body style="margin: 0px"> <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> <script type='module' src="./main.js"></script>
</body> </body>

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

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

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

@ -6,9 +6,11 @@ using Avalonia.Controls.Platform;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
using Avalonia.Platform.Storage;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Web.Interop; using Avalonia.Web.Interop;
using Avalonia.Web.Skia;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Web namespace Avalonia.Web
@ -23,6 +25,7 @@ namespace Avalonia.Web
private readonly JSObject _canvas; private readonly JSObject _canvas;
private readonly JSObject _nativeControlsContainer; private readonly JSObject _nativeControlsContainer;
private readonly JSObject _inputElement; private readonly JSObject _inputElement;
private readonly JSObject? _splash;
private GLInfo? _jsGlInfo = null; private GLInfo? _jsGlInfo = null;
private double _dpi = 1; private double _dpi = 1;
@ -59,11 +62,23 @@ namespace Avalonia.Web
_inputElement = hostContent.GetPropertyAsJSObject("inputElement") _inputElement = hostContent.GetPropertyAsJSObject("inputElement")
?? throw new InvalidOperationException("InputElement cannot be null"); ?? throw new InvalidOperationException("InputElement cannot be null");
_splash = DomHelper.GetElementById("avalonia-splash");
_canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}"); _canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}");
_topLevelImpl = new BrowserTopLevelImpl(this); _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) => _topLevelImpl.SetCssCursor = (cursor) =>
{ {
InputHelper.SetCursor(_containerElement, cursor); // macOS InputHelper.SetCursor(_containerElement, cursor); // macOS
@ -318,14 +333,7 @@ namespace Avalonia.Web
internal INativeControlHostImpl GetNativeControlHostImpl() internal INativeControlHostImpl GetNativeControlHostImpl()
{ {
throw new NotImplementedException(); return new BrowserNativeControlHost(_nativeControlsContainer);
//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");
} }
private void ForceBlit() 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;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Web.Skia;
namespace Avalonia.Web namespace Avalonia.Web
{ {

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

@ -11,8 +11,8 @@ using Avalonia.Platform;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Web.Skia;
#nullable enable using Avalonia.Web.Storage;
namespace Avalonia.Web namespace Avalonia.Web
{ {
@ -35,13 +35,11 @@ namespace Avalonia.Web
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
_touchDevice = new TouchDevice(); _touchDevice = new TouchDevice();
_penDevice = new PenDevice(); _penDevice = new PenDevice();
NativeControlHost = _avaloniaView.GetNativeControlHostImpl();
} }
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
public void SetClientSize(Size newSize, double dpi) public void SetClientSize(Size newSize, double dpi)
{ {
if (Math.Abs(RenderScaling - dpi) > 0.0001) if (Math.Abs(RenderScaling - dpi) > 0.0001)
@ -222,7 +220,7 @@ namespace Avalonia.Web
public ITextInputMethodImpl TextInputMethod => _avaloniaView; public ITextInputMethodImpl TextInputMethod => _avaloniaView;
public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl(); public INativeControlHostImpl? NativeControlHost { get; }
public IStorageProvider StorageProvider => _avaloniaView.GetStorageProvider(); 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 internal static partial class DomHelper
{ {
[JSImport("globalThis.document.getElementById")] [JSImport("globalThis.document.getElementById")]
internal static partial JSObject GetElementById(string id); internal static partial JSObject? GetElementById(string id);
[JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")] [JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")]
public static partial JSObject CreateAvaloniaHost(JSObject element); 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")] [JSImport("SizeWatcher.observe", "avalonia.ts")]
public static partial JSObject ObserveSize( public static partial JSObject ObserveSize(
JSObject canvas, JSObject canvas,

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

@ -1,8 +1,6 @@
using System; using System;
using System.Runtime.InteropServices.JavaScript; using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks; using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Avalonia.Web.Interop; namespace Avalonia.Web.Interop;
@ -39,7 +37,7 @@ internal static partial class InputHelper
Func<JSObject, bool> pointerUp, Func<JSObject, bool> pointerUp,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] [JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> wheel); Func<JSObject, bool> wheel);
[JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")] [JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")]
public static partial void SubscribeInputEvents( public static partial void SubscribeInputEvents(
@ -72,9 +70,9 @@ internal static partial class InputHelper
[JSImport("InputHelper.setBounds", "avalonia.ts")] [JSImport("InputHelper.setBounds", "avalonia.ts")]
public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret); 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(); public static partial Task<string> ReadClipboardTextAsync();
[JSImport("navigator.clipboard.writeText")] [JSImport("globalThis.navigator.clipboard.writeText")]
public static partial Task WriteClipboardTextAsync(string text); 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); public static partial void Seek(JSObject stream, [JSMarshalAs<JSType.Number>] long position);
[JSImport("StreamHelper.truncate", "avalonia.ts")] [JSImport("StreamHelper.truncate", "avalonia.ts")]
public static partial void Truncate(JSObject stream, [JSMarshalAs<JSType.Number>] long position); public static partial void Truncate(JSObject stream, [JSMarshalAs<JSType.Number>] long size);
[JSImport("StreamHelper.write", "avalonia.ts")]
public static partial void Write(JSObject stream, [JSMarshalAs<JSType.MemoryView>] Span<byte> data);
[JSImport("StreamHelper.write", "avalonia.ts")] [JSImport("StreamHelper.write", "avalonia.ts")]
public static partial Task WriteAsync(JSObject stream, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> data); 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")] [JSImport("StreamHelper.close", "avalonia.ts")]
public static partial Task CloseAsync(JSObject stream); 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")] [JSImport("StreamHelper.byteLength", "avalonia.ts")]
[return: JSMarshalAs<JSType.Number>] [return: JSMarshalAs<JSType.Number>]
public static partial long ByteLength(JSObject stream); public static partial long ByteLength(JSObject stream);
[JSImport("StreamHelper.sliceToArray", "avalonia.ts")] [JSImport("StreamHelper.sliceArrayBuffer", "avalonia.ts")]
[return: JSMarshalAs<JSType.MemoryView>] private static partial Task<JSObject> SliceToArrayBuffer(JSObject stream, [JSMarshalAs<JSType.Number>] long offset, int count);
public static partial Span<byte> Slice(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); using var buffer = await SliceToArrayBuffer(stream, offset, count);
return BufferToArray(buffer); 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 System.Collections.Generic;
using Avalonia.Skia; using Avalonia.Skia;
namespace Avalonia.Web namespace Avalonia.Web.Skia
{ {
public class BrowserSkiaGpu : ISkiaGpu 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 Avalonia.Skia;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Web namespace Avalonia.Web.Skia
{ {
internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession 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 Avalonia.Skia;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Web namespace Avalonia.Web.Skia
{ {
internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget 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 Avalonia.Skia;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Web namespace Avalonia.Web.Skia
{ {
internal class BrowserSkiaRasterSurface : IBrowserSkiaSurface, IFramebufferPlatformSurface, IDisposable 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 Avalonia.Web.Interop;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Web namespace Avalonia.Web.Skia
{ {
internal class BrowserSkiaSurface : IBrowserSkiaSurface internal class BrowserSkiaSurface : IBrowserSkiaSurface
{ {
@ -14,7 +14,7 @@ namespace Avalonia.Web
Scaling = scaling; Scaling = scaling;
Origin = origin; Origin = origin;
} }
public SKColorType ColorType { get; set; } public SKColorType ColorType { get; set; }
public PixelSize Size { 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 internal interface IBrowserSkiaSurface
{ {

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

@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace Avalonia.Web.Storage; namespace Avalonia.Web.Storage;
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings [System.Runtime.Versioning.SupportedOSPlatform("browser")]
internal class BlobReadableStream : Stream internal class BlobReadableStream : Stream
{ {
private JSObject? _jSReference; private JSObject? _jSReference;
@ -20,7 +20,7 @@ internal class BlobReadableStream : Stream
_length = StreamHelper.ByteLength(JSReference); _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; public override bool CanRead => true;
@ -55,21 +55,8 @@ internal class BlobReadableStream : Stream
=> throw new NotSupportedException(); => throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count) 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); throw new InvalidOperationException("Browser supports only ReadAsync");
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;
} }
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 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 numBytesToRead = (int)Math.Min(buffer.Length, Length - _position);
var bytesRead = await StreamHelper.SliceAsync(JSReference, _position, numBytesToRead); 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."); throw new EndOfStreamException("Failed to read the requested number of bytes from the stream.");
} }
_position += bytesRead.Count; _position += bytesRead.Length;
bytesRead.AsMemory().CopyTo(buffer); bytesRead.CopyTo(buffer);
return bytesRead.Count; return bytesRead.Length;
} }
protected override void Dispose(bool disposing) 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; 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 // Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream
internal sealed class JSWriteableStream : Stream internal sealed class WriteableStream : Stream
{ {
private JSObject? _jSReference; private JSObject? _jSReference;
// Unfortunatelly we can't read current length/position, so we need to keep it C#-side only. // Unfortunatelly we can't read current length/position, so we need to keep it C#-side only.
private long _length, _position; private long _length, _position;
internal JSWriteableStream(JSObject jSReference, long initialLength) internal WriteableStream(JSObject jSReference, long initialLength)
{ {
_jSReference = jSReference; _jSReference = jSReference;
_length = initialLength; _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; public override bool CanRead => false;
@ -75,20 +75,7 @@ internal sealed class JSWriteableStream : Stream
public override void Write(byte[] buffer, int offset, int count) public override void Write(byte[] buffer, int offset, int count)
{ {
StreamHelper.Write(JSReference, buffer.AsSpan(offset, count)); throw new InvalidOperationException("Browser supports only WriteAsync");
}
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);
} }
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
@ -110,7 +97,7 @@ internal sealed class JSWriteableStream : Stream
_jSReference = null; _jSReference = null;
try try
{ {
StreamHelper.Close(JSReference); _ = StreamHelper.CloseAsync(jsReference);
} }
finally finally
{ {
@ -126,7 +113,7 @@ internal sealed class JSWriteableStream : Stream
_jSReference = null; _jSReference = null;
try try
{ {
await StreamHelper.CloseAsync(JSReference); await StreamHelper.CloseAsync(jsReference);
} }
finally 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", target: "es2016",
platform: "browser", platform: "browser",
sourcemap: "linked", sourcemap: "linked",
loader: {".ts": "ts"} loader: { ".ts": "ts" }
}) })
.then(() => console.log("⚡ Done")) .then(() => console.log("⚡ Done"))
.catch(() => process.exit(1)); .catch(() => process.exit(1));

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

@ -1,18 +1,20 @@
import { RuntimeAPI } from "../types/dotnet"; import { RuntimeAPI } from "../types/dotnet";
import { SizeWatcher } from "./avalonia/canvas"; import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas";
import { DpiWatcher } from "./avalonia/canvas";
import { Canvas } from "./avalonia/canvas";
import { InputHelper } from "./avalonia/input"; import { InputHelper } from "./avalonia/input";
import { AvaloniaDOM } from "./avalonia/dom"; 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> { export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> {
api.setModuleImports("avalonia.ts", { api.setModuleImports("avalonia.ts", {
Caniuse,
Canvas, Canvas,
InputHelper, InputHelper,
SizeWatcher, SizeWatcher,
DpiWatcher, DpiWatcher,
AvaloniaDOM, 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; interface SKGLViewInfo {
type SKGLViewInfo = {
context: WebGLRenderingContext | WebGL2RenderingContext | undefined; context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
fboId: number; fboId: number;
stencil: number; stencil: number;
@ -9,8 +7,8 @@ type SKGLViewInfo = {
} }
type CanvasElement = { type CanvasElement = {
Canvas: Canvas | undefined Canvas: Canvas | undefined;
} & HTMLCanvasElement } & HTMLCanvasElement;
export class Canvas { export class Canvas {
static elements: Map<string, HTMLCanvasElement>; static elements: Map<string, HTMLCanvasElement>;
@ -24,22 +22,24 @@ export class Canvas {
newHeight?: number; newHeight?: number;
public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null { public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null {
var view = Canvas.init(true, element, elementId, renderFrameCallback); const view = Canvas.init(true, element, elementId, renderFrameCallback);
if (!view || !view.glInfo) if (!view || !view.glInfo) {
return null; return null;
}
return view.glInfo; return view.glInfo;
} }
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): Canvas | null { static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): Canvas | null {
var htmlCanvas = element as CanvasElement; const htmlCanvas = element as CanvasElement;
if (!htmlCanvas) { if (!htmlCanvas) {
console.error(`No canvas element was provided.`); console.error("No canvas element was provided.");
return null; return null;
} }
if (!Canvas.elements) if (!Canvas.elements) {
Canvas.elements = new Map<string, HTMLCanvasElement>(); Canvas.elements = new Map<string, HTMLCanvasElement>();
}
Canvas.elements.set(elementId, element); Canvas.elements.set(elementId, element);
const view = new Canvas(useGL, element, renderFrameCallback); const view = new Canvas(useGL, element, renderFrameCallback);
@ -56,16 +56,16 @@ export class Canvas {
if (useGL) { if (useGL) {
const ctx = Canvas.createWebGLContext(element); const ctx = Canvas.createWebGLContext(element);
if (!ctx) { if (!ctx) {
console.error(`Failed to create WebGL context: err ${ctx}`); console.error("Failed to create WebGL context");
return; return;
} }
var GL = (globalThis as any).AvaloniaGL; const GL = (globalThis as any).AvaloniaGL;
// make current // make current
GL.makeContextCurrent(ctx); GL.makeContextCurrent(ctx);
var GLctx = GL.currentContext.GLctx as WebGLRenderingContext; const GLctx = GL.currentContext.GLctx as WebGLRenderingContext;
// read values // read values
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
@ -75,17 +75,17 @@ export class Canvas {
fboId: fbo ? fbo.id : 0, fboId: fbo ? fbo.id : 0,
stencil: GLctx.getParameter(GLctx.STENCIL_BITS), stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES) 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; this.renderLoopEnabled = enable;
// either start the new frame or cancel the existing one // either start the new frame or cancel the existing one
if (enable) { if (enable) {
//console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`); // console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
this.requestAnimationFrame(); this.requestAnimationFrame();
} else if (this.renderLoopRequest !== 0) { } else if (this.renderLoopRequest !== 0) {
window.cancelAnimationFrame(this.renderLoopRequest); 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 // optionally update the render loop
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) {
this.setEnableRenderLoop(renderLoop); this.setEnableRenderLoop(renderLoop);
}
// skip because we have a render loop // skip because we have a render loop
if (this.renderLoopRequest !== 0) if (this.renderLoopRequest !== 0) {
return; return;
}
// add the draw to the next frame // add the draw to the next frame
this.renderLoopRequest = window.requestAnimationFrame(() => { this.renderLoopRequest = window.requestAnimationFrame(() => {
if (this.glInfo) { if (this.glInfo) {
var GL = (globalThis as any).AvaloniaGL; const GL = (globalThis as any).AvaloniaGL;
// make current // make current
GL.makeContextCurrent(this.glInfo.context); GL.makeContextCurrent(this.glInfo.context);
} }
if (this.htmlCanvas.width != this.newWidth) { if (this.htmlCanvas.width !== this.newWidth) {
this.htmlCanvas.width = this.newWidth || 0; this.htmlCanvas.width = this.newWidth ?? 0;
} }
if (this.htmlCanvas.height != this.newHeight) { if (this.htmlCanvas.height !== this.newHeight) {
this.htmlCanvas.height = this.newHeight || 0; this.htmlCanvas.height = this.newHeight ?? 0;
} }
this.renderFrameCallback(); this.renderFrameCallback();
this.renderLoopRequest = 0; this.renderLoopRequest = 0;
// we may want to draw the next frame // we may want to draw the next frame
if (this.renderLoopEnabled) if (this.renderLoopEnabled) {
this.requestAnimationFrame(); this.requestAnimationFrame();
}
}); });
} }
public setCanvasSize(width: number, height: number) { public setCanvasSize(width: number, height: number): void {
this.newWidth = width; this.newWidth = width;
this.newHeight = height; this.newHeight = height;
if (this.htmlCanvas.width != this.newWidth) { if (this.htmlCanvas.width !== this.newWidth) {
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; this.htmlCanvas.height = this.newHeight;
} }
if (this.glInfo) { if (this.glInfo) {
var GL = (globalThis as any).AvaloniaGL; const GL = (globalThis as any).AvaloniaGL;
// make current // make current
GL.makeContextCurrent(this.glInfo.context); 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; const htmlCanvas = element as CanvasElement;
if (!htmlCanvas || !htmlCanvas.Canvas) if (!htmlCanvas || !htmlCanvas.Canvas) {
return; return;
}
htmlCanvas.Canvas.setCanvasSize(width, height); 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; const htmlCanvas = element as CanvasElement;
if (!htmlCanvas || !htmlCanvas.Canvas) if (!htmlCanvas || !htmlCanvas.Canvas) {
return; return;
}
htmlCanvas.Canvas.requestAnimationFrame(renderLoop); htmlCanvas.Canvas.requestAnimationFrame(renderLoop);
} }
@ -176,15 +181,15 @@ export class Canvas {
minorVersion: 0, minorVersion: 0,
enableExtensionsByDefault: 1, enableExtensionsByDefault: 1,
explicitSwapControl: 0, 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); let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
if (!ctx && contextAttributes.majorVersion > 1) { 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.majorVersion = 1;
contextAttributes.minorVersion = 0; contextAttributes.minorVersion = 0;
ctx = GL.createContext(htmlCanvas, contextAttributes); ctx = GL.createContext(htmlCanvas, contextAttributes);
@ -196,9 +201,9 @@ export class Canvas {
type SizeWatcherElement = { type SizeWatcherElement = {
SizeWatcher: SizeWatcherInstance; SizeWatcher: SizeWatcherInstance;
} & HTMLElement } & HTMLElement;
type SizeWatcherInstance = { interface SizeWatcherInstance {
callback: (width: number, height: number) => void; callback: (width: number, height: number) => void;
} }
@ -206,17 +211,16 @@ export class SizeWatcher {
static observer: ResizeObserver; static observer: ResizeObserver;
static elements: Map<string, HTMLElement>; static elements: Map<string, HTMLElement>;
public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void) { public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void): void {
if (!element || !callback) if (!element || !callback) {
return; return;
}
//console.info(`Adding size watcher observation with callback ${callback._id}...`);
SizeWatcher.init(); SizeWatcher.init();
const watcherElement = element as SizeWatcherElement; const watcherElement = element as SizeWatcherElement;
watcherElement.SizeWatcher = { watcherElement.SizeWatcher = {
callback: callback callback
}; };
SizeWatcher.elements.set(elementId, element); SizeWatcher.elements.set(elementId, element);
@ -225,38 +229,38 @@ export class SizeWatcher {
SizeWatcher.invoke(element); SizeWatcher.invoke(element);
} }
public static unobserve(elementId: string) { public static unobserve(elementId: string): void {
if (!elementId || !SizeWatcher.observer) if (!elementId || !SizeWatcher.observer) {
return; return;
}
//console.info('Removing size watcher observation...'); const element = SizeWatcher.elements.get(elementId);
if (element) {
const element = SizeWatcher.elements.get(elementId)!; SizeWatcher.elements.delete(elementId);
SizeWatcher.observer.unobserve(element);
SizeWatcher.elements.delete(elementId); }
SizeWatcher.observer.unobserve(element);
} }
static init() { static init(): void {
if (SizeWatcher.observer) if (SizeWatcher.observer) {
return; return;
}
//console.info('Starting size watcher...');
SizeWatcher.elements = new Map<string, HTMLElement>(); SizeWatcher.elements = new Map<string, HTMLElement>();
SizeWatcher.observer = new ResizeObserver((entries) => { SizeWatcher.observer = new ResizeObserver((entries) => {
for (let entry of entries) { for (const entry of entries) {
SizeWatcher.invoke(entry.target); SizeWatcher.invoke(entry.target);
} }
}); });
} }
static invoke(element: Element) { static invoke(element: Element): void {
const watcherElement = element as SizeWatcherElement; const watcherElement = element as SizeWatcherElement;
const instance = watcherElement.SizeWatcher; const instance = watcherElement.SizeWatcher;
if (!instance || !instance.callback) if (!instance || !instance.callback) {
return; return;
}
return instance.callback(element.clientWidth, element.clientHeight); return instance.callback(element.clientWidth, element.clientHeight);
} }
@ -267,13 +271,11 @@ export class DpiWatcher {
static timerId: number; static timerId: number;
static callback: (old: number, newdpi: number) => void; static callback: (old: number, newdpi: number) => void;
public static getDpi() { public static getDpi(): number {
return window.devicePixelRatio; return window.devicePixelRatio;
} }
public static start(callback: (old: number, newdpi: number) => void) : number { public static start(callback: (old: number, newdpi: number) => void): number {
//console.info(`Starting DPI watcher with callback ${callback._id}...`);
DpiWatcher.lastDpi = window.devicePixelRatio; DpiWatcher.lastDpi = window.devicePixelRatio;
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
DpiWatcher.callback = callback; DpiWatcher.callback = callback;
@ -281,17 +283,14 @@ export class DpiWatcher {
return DpiWatcher.lastDpi; return DpiWatcher.lastDpi;
} }
public static stop() { public static stop(): void {
//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
window.clearInterval(DpiWatcher.timerId); window.clearInterval(DpiWatcher.timerId);
//DpiWatcher.callback = undefined;
} }
static update() { static update(): void {
if (!DpiWatcher.callback) if (!DpiWatcher.callback) {
return; return;
}
const currentDpi = window.devicePixelRatio; const currentDpi = window.devicePixelRatio;
const lastDpi = DpiWatcher.lastDpi; 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 { export class CaretHelper {
public static getCaretCoordinates( public static getCaretCoordinates(
element: HTMLInputElement | HTMLTextAreaElement, element: HTMLInputElement | HTMLTextAreaElement,
@ -11,7 +11,7 @@ export class CaretHelper {
); );
} }
const debug = (options && options.debug) || false; const debug = options?.debug ?? false;
if (debug) { if (debug) {
const el = document.querySelector( const el = document.querySelector(
"#input-textarea-caret-position-mirror-div" "#input-textarea-caret-position-mirror-div"
@ -27,7 +27,7 @@ export class CaretHelper {
const style = div.style; const style = div.style;
const computed = window.getComputedStyle const computed = window.getComputedStyle
? window.getComputedStyle(element) ? 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"; const isInput = element.nodeName === "INPUT";
// Default textarea styles // Default textarea styles
@ -51,7 +51,7 @@ export class CaretHelper {
parseInt(computed.borderBottomWidth); parseInt(computed.borderBottomWidth);
const targetHeight = outerHeight + parseInt(computed.lineHeight); const targetHeight = outerHeight + parseInt(computed.lineHeight);
if (height > targetHeight) { if (height > targetHeight) {
style.lineHeight = height - outerHeight + "px"; style.lineHeight = `${height - outerHeight}px`;
} else if (height === targetHeight) { } else if (height === targetHeight) {
style.lineHeight = computed.lineHeight; style.lineHeight = computed.lineHeight;
} else { } else {
@ -67,8 +67,9 @@ export class CaretHelper {
if (isFirefox) { if (isFirefox) {
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275 // 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"; style.overflowY = "scroll";
}
} else { } else {
style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll' style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
} }
@ -88,9 +89,9 @@ export class CaretHelper {
div.appendChild(span); div.appendChild(span);
const coordinates = { const coordinates = {
top: span.offsetTop + parseInt(computed["borderTopWidth"]), top: span.offsetTop + parseInt(computed.borderTopWidth),
left: span.offsetLeft + parseInt(computed["borderLeftWidth"]), left: span.offsetLeft + parseInt(computed.borderLeftWidth),
height: parseInt(computed["lineHeight"]), height: parseInt(computed.lineHeight)
}; };
if (debug) { if (debug) {
@ -103,8 +104,7 @@ export class CaretHelper {
} }
} }
const properties = [
var properties = [
"direction", // RTL support "direction", // RTL support
"boxSizing", "boxSizing",
"width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does "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", "wordSpacing",
"tabSize", "tabSize",
"MozTabSize", "MozTabSize"
]; ];
const isBrowser = typeof window !== "undefined"; const isBrowser = typeof window !== "undefined";

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

@ -1,4 +1,8 @@
export class AvaloniaDOM { export class AvaloniaDOM {
public static addClass(element: HTMLElement, className: string): void {
element.classList.add(className);
}
static createAvaloniaHost(host: HTMLElement) { static createAvaloniaHost(host: HTMLElement) {
// Root element // Root element
host.classList.add("avalonia-container"); host.classList.add("avalonia-container");
@ -7,26 +11,24 @@ export class AvaloniaDOM {
// Rendering target canvas // Rendering target canvas
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.classList.add('avalonia-canvas'); canvas.classList.add("avalonia-canvas");
canvas.style.backgroundColor = "#ccc"; canvas.style.backgroundColor = "#ccc";
canvas.style.width = "100%"; canvas.style.width = "100%";
canvas.style.height = "100%"; canvas.style.height = "100%";
canvas.style.position = "absolute"; canvas.style.position = "absolute";
host.appendChild(canvas);
// Native controls host // Native controls host
const nativeHost = document.createElement("div"); const nativeHost = document.createElement("div");
nativeHost.classList.add('avalonia-native-host'); nativeHost.classList.add("avalonia-native-host");
nativeHost.style.left = "0px"; nativeHost.style.left = "0px";
nativeHost.style.top = "0px"; nativeHost.style.top = "0px";
nativeHost.style.width = "100%"; nativeHost.style.width = "100%";
nativeHost.style.height = "100%"; nativeHost.style.height = "100%";
nativeHost.style.position = "absolute"; nativeHost.style.position = "absolute";
host.appendChild(nativeHost);
// IME // IME
const inputElement = document.createElement("input"); const inputElement = document.createElement("input");
inputElement.classList.add('avalonia-input-element'); inputElement.classList.add("avalonia-input-element");
inputElement.autocapitalize = "none"; inputElement.autocapitalize = "none";
inputElement.type = "text"; inputElement.type = "text";
inputElement.spellcheck = false; inputElement.spellcheck = false;
@ -43,7 +45,10 @@ export class AvaloniaDOM {
inputElement.onpaste = function () { return false; }; inputElement.onpaste = function () { return false; };
inputElement.oncopy = function () { return false; }; inputElement.oncopy = function () { return false; };
inputElement.oncut = function () { return false; }; inputElement.oncut = function () { return false; };
host.appendChild(inputElement);
host.prepend(inputElement);
host.prepend(nativeHost);
host.prepend(canvas);
return { return {
host, 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, None = 0,
Alt = 1, Alt = 1,
Control = 2, Control = 2,
@ -24,12 +23,7 @@ export class InputHelper {
public static subscribeKeyEvents( public static subscribeKeyEvents(
element: HTMLInputElement, element: HTMLInputElement,
keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean,
keyUpCallback: (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)
{
const keyDownHandler = (args: KeyboardEvent) => { const keyDownHandler = (args: KeyboardEvent) => {
if (keyDownCallback(args.code, args.key, this.getModifiers(args))) { if (keyDownCallback(args.code, args.key, this.getModifiers(args))) {
args.preventDefault(); args.preventDefault();
@ -52,14 +46,13 @@ export class InputHelper {
} }
public static subscribeTextEvents( public static subscribeTextEvents(
element: HTMLInputElement, element: HTMLInputElement,
inputCallback: (type: string, data: string | null) => boolean, inputCallback: (type: string, data: string | null) => boolean,
compositionStartCallback: (args: CompositionEvent) => boolean, compositionStartCallback: (args: CompositionEvent) => boolean,
compositionUpdateCallback: (args: CompositionEvent) => boolean, compositionUpdateCallback: (args: CompositionEvent) => boolean,
compositionEndCallback: (args: CompositionEvent) => boolean) { compositionEndCallback: (args: CompositionEvent) => boolean) {
const inputHandler = (args: Event) => { const inputHandler = (args: Event) => {
var inputEvent = args as InputEvent; const inputEvent = args as InputEvent;
// todo check cast // todo check cast
if (inputCallback(inputEvent.type, inputEvent.data)) { if (inputCallback(inputEvent.type, inputEvent.data)) {
@ -102,31 +95,27 @@ export class InputHelper {
pointerMoveCallback: (args: PointerEvent) => boolean, pointerMoveCallback: (args: PointerEvent) => boolean,
pointerDownCallback: (args: PointerEvent) => boolean, pointerDownCallback: (args: PointerEvent) => boolean,
pointerUpCallback: (args: PointerEvent) => boolean, pointerUpCallback: (args: PointerEvent) => boolean,
wheelCallback: (args: WheelEvent) => boolean, wheelCallback: (args: WheelEvent) => boolean
) { ) {
const pointerMoveHandler = (args: PointerEvent) => { const pointerMoveHandler = (args: PointerEvent) => {
if (pointerMoveCallback(args)) { if (pointerMoveCallback(args)) {
args.preventDefault(); args.preventDefault();
} }
}; };
const pointerDownHandler = (args: PointerEvent) => { const pointerDownHandler = (args: PointerEvent) => {
if (pointerDownCallback(args)) { if (pointerDownCallback(args)) {
args.preventDefault(); args.preventDefault();
} }
}; };
const pointerUpHandler = (args: PointerEvent) => { const pointerUpHandler = (args: PointerEvent) => {
if (pointerUpCallback(args)) { if (pointerUpCallback(args)) {
args.preventDefault(); args.preventDefault();
} }
}; };
const wheelHandler = (args: WheelEvent) => { const wheelHandler = (args: WheelEvent) => {
if (wheelCallback(args)) { if (wheelCallback(args)) {
args.preventDefault(); args.preventDefault();
} }
@ -137,8 +126,6 @@ export class InputHelper {
element.addEventListener("pointerup", pointerUpHandler); element.addEventListener("pointerup", pointerUpHandler);
element.addEventListener("wheel", wheelHandler); element.addEventListener("wheel", wheelHandler);
return () => { return () => {
element.removeEventListener("pointerover", pointerMoveHandler); element.removeEventListener("pointerover", pointerMoveHandler);
element.removeEventListener("pointerdown", pointerDownHandler); element.removeEventListener("pointerdown", pointerDownHandler);
@ -146,7 +133,7 @@ export class InputHelper {
element.removeEventListener("wheel", wheelHandler); element.removeEventListener("wheel", wheelHandler);
}; };
} }
public static subscribeInputEvents( public static subscribeInputEvents(
element: HTMLInputElement, element: HTMLInputElement,
inputCallback: (value: string) => boolean inputCallback: (value: string) => boolean
@ -179,18 +166,18 @@ export class InputHelper {
inputElement.style.left = (x).toFixed(0) + "px"; inputElement.style.left = (x).toFixed(0) + "px";
inputElement.style.top = (y).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.left = (x - left).toFixed(0) + "px";
inputElement.style.top = (y - top).toFixed(0) + "px"; inputElement.style.top = (y - top).toFixed(0) + "px";
} }
public static hide(inputElement: HTMLInputElement) { public static hide(inputElement: HTMLInputElement) {
inputElement.style.display = 'none'; inputElement.style.display = "none";
} }
public static show(inputElement: HTMLInputElement) { public static show(inputElement: HTMLInputElement) {
inputElement.style.display = 'block'; inputElement.style.display = "block";
} }
public static setSurroundingText(inputElement: HTMLInputElement, text: string, start: number, end: number) { public static setSurroundingText(inputElement: HTMLInputElement, text: string, start: number, end: number) {
@ -201,20 +188,16 @@ export class InputHelper {
inputElement.value = text; inputElement.value = text;
inputElement.setSelectionRange(start, end); inputElement.setSelectionRange(start, end);
inputElement.style.width = "20px"; inputElement.style.width = "20px";
inputElement.style.width = inputElement.scrollWidth + "px"; inputElement.style.width = `${inputElement.scrollWidth}px`;
} }
private static getModifiers(args: KeyboardEvent): RawInputModifiers { private static getModifiers(args: KeyboardEvent): RawInputModifiers {
var modifiers = RawInputModifiers.None; let modifiers = RawInputModifiers.None;
if (args.ctrlKey) if (args.ctrlKey) { modifiers |= RawInputModifiers.Control; }
modifiers |= RawInputModifiers.Control; if (args.altKey) { modifiers |= RawInputModifiers.Alt; }
if (args.altKey) if (args.shiftKey) { modifiers |= RawInputModifiers.Shift; }
modifiers |= RawInputModifiers.Alt; if (args.metaKey) { modifiers |= RawInputModifiers.Meta; }
if (args.shiftKey)
modifiers |= RawInputModifiers.Shift;
if (args.metaKey)
modifiers |= RawInputModifiers.Meta;
return modifiers; 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 { export { StorageItem, StorageItems } from "./storage/storageItem";
static isFileApiSupported(): boolean { export { StorageProvider } from "./storage/storageProvider";
return (globalThis as any).showOpenFilePicker !== undefined;
}
}

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", "name": "avalonia.web",
"scripts": { "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" "build": "node build.js"
}, },
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.39.6", "@types/emscripten": "^1.39.6",
"@types/wicg-file-system-access": "^2020.9.5", "@types/wicg-file-system-access": "^2020.9.5",
"typescript": "^4.7.4", "@typescript-eslint/eslint-plugin": "^5.38.1",
"esbuild": "^0.15.7" "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 dotnet: ModuleAPI["dotnet"];
declare const exit: ModuleAPI["exit"]; 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="WinRT\winrt.idl" CSharpInteropPath="WinRT\WinRT.Generated.cs" />
<MicroComIdl Include="Win32Com\win32.idl" CSharpInteropPath="Win32Com\Win32.Generated.cs" /> <MicroComIdl Include="Win32Com\win32.idl" CSharpInteropPath="Win32Com\Win32.Generated.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove="..\..\Shared\ModuleInitializer.cs" />
</ItemGroup>
<Import Project="$(MSBuildThisFileDirectory)\..\..\..\build\System.Drawing.Common.props" /> <Import Project="$(MSBuildThisFileDirectory)\..\..\..\build\System.Drawing.Common.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<PropertyGroup Label="Warnings"> <PropertyGroup Label="Warnings">

Loading…
Cancel
Save