Browse Source

Merge branch 'master' into fixes/rtbMeasureArrange

pull/9147/head
Benedikt Stebner 4 years ago
committed by GitHub
parent
commit
9094853056
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 3
      .gitmodules
  3. 0
      .nuke
  4. 148
      .nuke/build.schema.json
  5. 4
      .nuke/parameters.json
  6. 18
      Avalonia.sln
  7. 5
      azure-pipelines-integrationtests.yml
  8. 44
      azure-pipelines.yml
  9. 7
      build.cmd
  10. 50
      build.ps1
  11. 60
      build.sh
  12. 3
      dirs.proj
  13. 4
      global.json
  14. 20
      nukebuild/Build.cs
  15. 4
      nukebuild/BuildParameters.cs
  16. 12
      nukebuild/BuildTasksPatcher.cs
  17. 4
      nukebuild/DotNetConfigHelper.cs
  18. 14
      nukebuild/MicroComGen.cs
  19. 15
      nukebuild/Shims.cs
  20. 41
      nukebuild/_build.csproj
  21. 1
      nukebuild/il-repack
  22. 3
      samples/ControlCatalog/MainView.xaml
  23. 5
      samples/ControlCatalog/MainView.xaml.cs
  24. 66
      samples/ControlCatalog/Pages/AdornerLayerPage.xaml
  25. 48
      samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs
  26. 12
      samples/ControlCatalog/Pages/ScreenPage.cs
  27. 5
      samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
  28. 1
      samples/Sandbox/MainWindow.axaml
  29. 35
      src/Avalonia.Base/Logging/LogArea.cs
  30. 106
      src/Avalonia.Base/Media/FormattedText.cs
  31. 13
      src/Avalonia.Base/Media/Geometry.cs
  32. 11
      src/Avalonia.Base/Media/GlyphRun.cs
  33. 253
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  34. 2
      src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs
  35. 2
      src/Avalonia.Base/Platform/Storage/IStorageFile.cs
  36. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  37. 2
      src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs
  38. 2
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  39. 1
      src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
  40. 359
      src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs
  41. 28
      src/Avalonia.Controls/ComboBox.cs
  42. 14
      src/Avalonia.Controls/Documents/InlineCollection.cs
  43. 111
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  44. 34
      src/Avalonia.Controls/Window.cs
  45. 3
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  46. 19
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  47. 6
      src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
  48. 4
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  49. 2
      src/Avalonia.Themes.Simple/Controls/DatePicker.xaml
  50. 2
      src/Avalonia.Themes.Simple/Controls/TimePicker.xaml
  51. 2
      src/Avalonia.X11/X11Window.cs
  52. 2
      src/Shared/ModuleInitializer.cs
  53. 1
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  54. 41
      src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj
  55. 44
      src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs
  56. 5
      src/Web/Avalonia.Web.Sample/Logo.svg
  57. 19
      src/Web/Avalonia.Web.Sample/Program.cs
  58. 38
      src/Web/Avalonia.Web.Sample/app.css
  59. 11
      src/Web/Avalonia.Web.Sample/embed.js
  60. BIN
      src/Web/Avalonia.Web.Sample/favicon.ico
  61. 31
      src/Web/Avalonia.Web.Sample/index.html
  62. 19
      src/Web/Avalonia.Web.Sample/main.js
  63. 11
      src/Web/Avalonia.Web.Sample/runtimeconfig.template.json
  64. 55
      src/Web/Avalonia.Web/Avalonia.Web.csproj
  65. 5
      src/Web/Avalonia.Web/Avalonia.Web.props
  66. 7
      src/Web/Avalonia.Web/Avalonia.Web.targets
  67. 451
      src/Web/Avalonia.Web/AvaloniaView.cs
  68. 136
      src/Web/Avalonia.Web/BrowserNativeControlHost.cs
  69. 41
      src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs
  70. 226
      src/Web/Avalonia.Web/BrowserTopLevelImpl.cs
  71. 29
      src/Web/Avalonia.Web/ClipboardImpl.cs
  72. 95
      src/Web/Avalonia.Web/Cursor.cs
  73. 43
      src/Web/Avalonia.Web/Interop/CanvasHelper.cs
  74. 28
      src/Web/Avalonia.Web/Interop/DomHelper.cs
  75. 78
      src/Web/Avalonia.Web/Interop/InputHelper.cs
  76. 28
      src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs
  77. 55
      src/Web/Avalonia.Web/Interop/StorageHelper.cs
  78. 40
      src/Web/Avalonia.Web/Interop/StreamHelper.cs
  79. 30
      src/Web/Avalonia.Web/JSObjectControlHandle.cs
  80. 129
      src/Web/Avalonia.Web/Keycodes.cs
  81. 18
      src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs
  82. 26
      src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs
  83. 36
      src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs
  84. 39
      src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderTarget.cs
  85. 88
      src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs
  86. 30
      src/Web/Avalonia.Web/Skia/BrowserSkiaSurface.cs
  87. 9
      src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs
  88. 88
      src/Web/Avalonia.Web/Storage/BlobReadableStream.cs
  89. 257
      src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs
  90. 124
      src/Web/Avalonia.Web/Storage/WriteableStream.cs
  91. 68
      src/Web/Avalonia.Web/WebEmbeddableControlRoot.cs
  92. 48
      src/Web/Avalonia.Web/WinStubs.cs
  93. 105
      src/Web/Avalonia.Web/WindowingPlatform.cs
  94. 13
      src/Web/Avalonia.Web/interop.js
  95. 47
      src/Web/Avalonia.Web/webapp/.eslintrc.json
  96. 16
      src/Web/Avalonia.Web/webapp/build.js
  97. 20
      src/Web/Avalonia.Web/webapp/modules/avalonia.ts
  98. 13
      src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts
  99. 303
      src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts
  100. 149
      src/Web/Avalonia.Web/webapp/modules/avalonia/caretHelper.ts

1
.gitignore

@ -215,3 +215,4 @@ src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js
node_modules
src/Web/Avalonia.Web.Blazor/webapp/package-lock.json
src/Web/Avalonia.Web.Blazor/wwwroot
src/Web/Avalonia.Web/wwwroot

3
.gitmodules

@ -4,3 +4,6 @@
[submodule "src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github"]
path = src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
url = https://github.com/kekekeks/XamlX.git
[submodule "nukebuild/il-repack"]
path = nukebuild/il-repack
url = https://github.com/Gillibald/il-repack

0
.nuke

148
.nuke/build.schema.json

@ -0,0 +1,148 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Build Schema",
"$ref": "#/definitions/build",
"definitions": {
"build": {
"type": "object",
"properties": {
"Configuration": {
"type": "string",
"description": "configuration"
},
"Continue": {
"type": "boolean",
"description": "Indicates to continue a previously failed build attempt"
},
"ForceNugetVersion": {
"type": "string",
"description": "force-nuget-version"
},
"Help": {
"type": "boolean",
"description": "Shows the help text for this build assembly"
},
"Host": {
"type": "string",
"description": "Host for execution. Default is 'automatic'",
"enum": [
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitbucket",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"Rider",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI",
"VisualStudio",
"VSCode"
]
},
"NoLogo": {
"type": "boolean",
"description": "Disables displaying the NUKE logo"
},
"Partition": {
"type": "string",
"description": "Partition to use on CI"
},
"Plan": {
"type": "boolean",
"description": "Shows the execution plan (HTML)"
},
"Profile": {
"type": "array",
"description": "Defines the profiles to load",
"items": {
"type": "string"
}
},
"Root": {
"type": "string",
"description": "Root directory during build execution"
},
"Skip": {
"type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies",
"items": {
"type": "string",
"enum": [
"CiAzureLinux",
"CiAzureOSX",
"CiAzureWindows",
"Clean",
"Compile",
"CompileHtmlPreviewer",
"CompileNative",
"CreateIntermediateNugetPackages",
"CreateNugetPackages",
"GenerateCppHeaders",
"Package",
"RunCoreLibsTests",
"RunDesignerTests",
"RunHtmlPreviewerTests",
"RunLeakTests",
"RunRenderTests",
"RunTests",
"ZipFiles"
]
}
},
"SkipPreviewer": {
"type": "boolean",
"description": "skip-previewer"
},
"SkipTests": {
"type": "boolean",
"description": "skip-tests"
},
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded. Default is Avalonia.sln"
},
"Target": {
"type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'",
"items": {
"type": "string",
"enum": [
"CiAzureLinux",
"CiAzureOSX",
"CiAzureWindows",
"Clean",
"Compile",
"CompileHtmlPreviewer",
"CompileNative",
"CreateIntermediateNugetPackages",
"CreateNugetPackages",
"GenerateCppHeaders",
"Package",
"RunCoreLibsTests",
"RunDesignerTests",
"RunHtmlPreviewerTests",
"RunLeakTests",
"RunRenderTests",
"RunTests",
"ZipFiles"
]
}
},
"Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'",
"enum": [
"Minimal",
"Normal",
"Quiet",
"Verbose"
]
}
}
}
}
}

4
.nuke/parameters.json

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

18
Avalonia.sln

@ -212,7 +212,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPick
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web", "src\Web\Avalonia.Web\Avalonia.Web.csproj", "{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Sample", "src\Web\Avalonia.Web.Sample\Avalonia.Web.Sample.csproj", "{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox", "samples\MobileSandbox\MobileSandbox.csproj", "{3B8519C1-2F51-4F12-A348-120AB91D4532}"
EndProject
@ -407,9 +411,7 @@ Global
{BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.Build.0 = Release|Any CPU
{3F00BC43-5095-477F-93D8-E65B08179A00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F00BC43-5095-477F-93D8-E65B08179A00}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F00BC43-5095-477F-93D8-E65B08179A00}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F00BC43-5095-477F-93D8-E65B08179A00}.Release|Any CPU.Build.0 = Release|Any CPU
{41B02319-965D-4945-8005-C1A3D1224165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41B02319-965D-4945-8005-C1A3D1224165}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41B02319-965D-4945-8005-C1A3D1224165}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -510,6 +512,14 @@ Global
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.Build.0 = Release|Any CPU
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Release|Any CPU.Build.0 = Release|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -583,6 +593,8 @@ Global
{2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
{3B8519C1-2F51-4F12-A348-120AB91D4532} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C90FE60B-B01E-4F35-91D6-379D6966030F} = {9B9E3891-2366-4253-A952-D08BCEB71098}

5
azure-pipelines-integrationtests.yml

@ -41,9 +41,9 @@ jobs:
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.202'
displayName: 'Use .NET Core SDK 6.0.401'
inputs:
version: 6.0.202
version: 6.0.401
- task: Windows Application Driver@0
inputs:
@ -57,6 +57,7 @@ jobs:
projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj'
- task: DotNetCoreCLI@2
retryCountOnTaskFailure: 3
inputs:
command: 'test'
projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'

44
azure-pipelines.yml

@ -6,7 +6,6 @@ jobs:
variables:
SolutionDir: '$(Build.SourcesDirectory)'
steps:
- task: PowerShell@2
displayName: Get PR Number
inputs:
@ -31,14 +30,20 @@ jobs:
vmImage: 'ubuntu-20.04'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.418'
displayName: 'Use .NET Core SDK 6.0.401'
inputs:
version: 3.1.418
version: 6.0.401
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.202'
displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12'
inputs:
version: 7.0.100-rc.1.22431.12
- task: CmdLine@2
displayName: 'Install Workloads'
inputs:
version: 6.0.202
script: |
dotnet workload install wasm-tools wasm-experimental
- task: CmdLine@2
displayName: 'Run Build'
@ -62,22 +67,21 @@ jobs:
vmImage: 'macos-12'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.418'
displayName: 'Use .NET Core SDK 6.0.401'
inputs:
version: 3.1.418
version: 6.0.401
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.202'
displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12'
inputs:
version: 6.0.202
version: 7.0.100-rc.1.22431.12
- task: CmdLine@2
displayName: 'Install Mono 5.18'
displayName: 'Install Workloads'
inputs:
script: |
curl -o ./mono.pkg https://download.mono-project.com/archive/5.18.0/macos-10-universal/MonoFramework-MDK-5.18.0.225.macos10.xamarin.universal.pkg
sudo installer -verbose -pkg ./mono.pkg -target /
dotnet workload install wasm-tools wasm-experimental
- task: CmdLine@2
displayName: 'Generate avalonia-native'
inputs:
@ -134,26 +138,26 @@ jobs:
SolutionDir: '$(Build.SourcesDirectory)'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.418'
displayName: 'Use .NET Core SDK 6.0.401'
inputs:
version: 3.1.418
version: 6.0.401
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.202'
displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12'
inputs:
version: 6.0.202
version: 7.0.100-rc.1.22431.12
- task: CmdLine@2
displayName: 'Install Workloads'
inputs:
script: |
dotnet workload install android ios
dotnet workload install android ios wasm-tools wasm-experimental
- task: CmdLine@2
displayName: 'Install Nuke'
inputs:
script: |
dotnet tool install --global Nuke.GlobalTool --version 0.24.0
dotnet tool install --global Nuke.GlobalTool --version 6.2.1
- task: CmdLine@2
displayName: 'Run Nuke'

7
build.cmd

@ -0,0 +1,7 @@
:; set -eo pipefail
:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
:; ${SCRIPT_DIR}/build.sh "$@"
:; exit $?
@ECHO OFF
powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*

50
build.ps1

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

60
build.sh

@ -1,16 +1,6 @@
#!/usr/bin/env bash
echo $(bash --version 2>&1 | head -n 1)
#CUSTOMPARAM=0
BUILD_ARGUMENTS=()
for i in "$@"; do
case $(echo $1 | awk '{print tolower($0)}') in
# -custom-param) CUSTOMPARAM=1;;
*) BUILD_ARGUMENTS+=("$1") ;;
esac
shift
done
bash --version 2>&1 | head -n 1
set -eo pipefail
SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
@ -20,11 +10,53 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
###########################################################################
BUILD_PROJECT_FILE="$SCRIPT_DIR/nukebuild/_build.csproj"
TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
DOTNET_CHANNEL="Current"
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
export NUGET_XMLDOC_MODE="skip"
export DOTNET_MULTILEVEL_LOOKUP=0
dotnet --info
###########################################################################
# EXECUTION
###########################################################################
dotnet run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]}
function FirstJsonValue {
perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
export DOTNET_EXE="$(command -v dotnet)"
else
# Download install script
DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
mkdir -p "$TEMP_DIRECTORY"
curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
chmod +x "$DOTNET_INSTALL_FILE"
# If global.json exists, load expected version
if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
if [[ "$DOTNET_VERSION" == "" ]]; then
unset DOTNET_VERSION
fi
fi
# Install by channel or version
DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
if [[ -z ${DOTNET_VERSION+x} ]]; then
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
else
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
fi
export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
fi
echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)"
"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"

3
dirs.proj

@ -23,12 +23,13 @@
<ProjectReference Remove="samples/ControlCatalog.Desktop/*.*proj" />
</ItemGroup>
<!-- Build android and iOS projects only on Windows, where we have installed android workload -->
<ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows'))">
<ProjectReference Remove="src/Android/**/*.*proj" />
<ProjectReference Remove="src/iOS/**/*.*proj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SlnGen" Version="2.0.40" PrivateAssets="all" />
<PackageReference Include="Microsoft.VisualStudio.SlnGen" Version="8.5.17" PrivateAssets="all" />
</ItemGroup>
</Project>

4
global.json

@ -1,8 +1,4 @@
{
"sdk": {
"version": "6.0.202",
"rollForward": "latestFeature"
},
"msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43",
"MSBuild.Sdk.Extras": "3.0.22",

20
nukebuild/Build.cs

@ -23,6 +23,7 @@ using static Nuke.Common.Tools.MSBuild.MSBuildTasks;
using static Nuke.Common.Tools.DotNet.DotNetTasks;
using static Nuke.Common.Tools.Xunit.XunitTasks;
using static Nuke.Common.Tools.VSWhere.VSWhereTasks;
using MicroCom.CodeGenerator;
/*
Before editing this file, install support plugin for your IDE,
@ -163,7 +164,7 @@ partial class Build : NukeBuild
.EnableNoBuild()
.EnableNoRestore()
.When(Parameters.PublishTestResults, _ => _
.SetLogger("trx")
.SetLoggers("trx")
.SetResultsDirectory(Parameters.TestResultsRoot)));
}
}
@ -215,8 +216,6 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.DesignerSupport.Tests");
});
[PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit;
Target RunLeakTests => _ => _
.OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
.DependsOn(Compile)
@ -224,12 +223,9 @@ partial class Build : NukeBuild
{
void DoMemoryTest()
{
var testAssembly = "tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll";
DotMemoryUnit(
$"{XunitPath.DoubleQuoteIfNeeded()} --propagate-exit-code -- {testAssembly}",
timeout: 120_000);
RunCoreTest("Avalonia.LeakTests");
}
ControlFlow.ExecuteWithRetry(DoMemoryTest, waitInSeconds: 3);
ControlFlow.ExecuteWithRetry(DoMemoryTest, delay: TimeSpan.FromMilliseconds(3));
});
Target ZipFiles => _ => _
@ -283,6 +279,14 @@ partial class Build : NukeBuild
.DependsOn(Package)
.DependsOn(ZipFiles);
Target GenerateCppHeaders => _ => _.Executes(() =>
{
var file = MicroComCodeGenerator.Parse(
File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl"));
File.WriteAllText(RootDirectory / "native" / "Avalonia.Native" / "inc" / "avalonia-native.h",
file.GenerateCppHeader());
});
public static int Main() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)

4
nukebuild/BuildParameters.cs

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

12
nukebuild/BuildTasksPatcher.cs

@ -17,8 +17,12 @@ public class BuildTasksPatcher
{
if (entry.Name == "Avalonia.Build.Tasks.dll")
{
var temp = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".dll");
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var temp = Path.Combine(tempDir, Guid.NewGuid() + ".dll");
var output = temp + ".output";
File.Copy(typeof(Microsoft.Build.Framework.ITask).Assembly.GetModules()[0].FullyQualifiedName,
Path.Combine(tempDir, "Microsoft.Build.Framework.dll"));
var patched = new MemoryStream();
try
{
@ -57,10 +61,8 @@ public class BuildTasksPatcher
{
try
{
if (File.Exists(temp))
File.Delete(temp);
if (File.Exists(output))
File.Delete(output);
if(Directory.Exists(tempDir))
Directory.Delete(tempDir, true);
}
catch
{

4
nukebuild/DotNetConfigHelper.cs

@ -46,7 +46,7 @@ public class DotNetConfigHelper
public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity)
{
Build = Build?.SetVerbosity(verbosity);
Pack = Pack?.SetVerbostiy(verbosity);
Pack = Pack?.SetVerbosity(verbosity);
Test = Test?.SetVerbosity(verbosity);
return this;
}
@ -54,4 +54,4 @@ public class DotNetConfigHelper
public static implicit operator DotNetConfigHelper(DotNetBuildSettings s) => new DotNetConfigHelper(s);
public static implicit operator DotNetConfigHelper(DotNetPackSettings s) => new DotNetConfigHelper(s);
public static implicit operator DotNetConfigHelper(DotNetTestSettings s) => new DotNetConfigHelper(s);
}
}

14
nukebuild/MicroComGen.cs

@ -1,14 +0,0 @@
using System.IO;
using MicroCom.CodeGenerator;
using Nuke.Common;
partial class Build : NukeBuild
{
Target GenerateCppHeaders => _ => _.Executes(() =>
{
var file = MicroComCodeGenerator.Parse(
File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl"));
File.WriteAllText(RootDirectory / "native" / "Avalonia.Native" / "inc" / "avalonia-native.h",
file.GenerateCppHeader());
});
}

15
nukebuild/Shims.cs

@ -49,7 +49,11 @@ public partial class Build
{
if (fsEntry is FileInfo)
{
#if NET6
var relPath = Path.GetRelativePath(rootPath, fsEntry.FullName);
#else
var relPath = GetRelativePath(rootPath, fsEntry.FullName);
#endif
AddFile(fsEntry.FullName, relPath);
}
}
@ -78,6 +82,17 @@ public partial class Build
}
}
private static string GetRelativePath(string relativeTo, string path)
{
var uri = new Uri(relativeTo);
var rel = Uri.UnescapeDataString(uri.MakeRelativeUri(new Uri(path)).ToString()).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
if (rel.Contains(Path.DirectorySeparatorChar.ToString()) == false)
{
rel = $".{Path.DirectorySeparatorChar}{rel}";
}
return rel;
}
class NumergeNukeLogger : INumergeLogger
{
public void Log(NumergeLogLevel level, string message)

41
nukebuild/_build.csproj

@ -1,41 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<RootNamespace></RootNamespace>
<IsPackable>False</IsPackable>
<NoWarn>CS0649;CS0169</NoWarn>
<NoWarn>CS0649;CS0169;SYSLIB0011</NoWarn>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<Import Project="..\build\JetBrains.dotMemoryUnit.props" />
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="5.0.0" />
<PackageReference Include="xunit.runner.console" Version="2.3.1" />
<PackageReference Include="Nuke.Common" Version="6.2.1" />
<PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " />
<PackageReference Include="ILRepack.NETStandard" Version="2.0.4" />
<PackageReference Include="MicroCom.CodeGenerator" Version="0.10.4" />
<!-- Keep in sync with Avalonia.Build.Tasks -->
<PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="SourceLink" Version="1.1.0" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Build.Framework" Version="17.3.1" PrivateAssets="All" />
<PackageReference Include="xunit.runner.console" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<NukeMetadata Include="**\*.json" Exclude="bin\**;obj\**" />
<NukeExternalFiles Include="**\*.*.ext" Exclude="bin\**;obj\**" />
<None Remove="*.csproj.DotSettings;*.ref.*.txt" />
<!-- Common build related files -->
<None Include="..\build.ps1" />
<None Include="..\build.sh" />
<None Include="..\.nuke" />
<None Include="..\global.json" Condition="Exists('..\global.json')" />
<None Include="..\nuget.config" Condition="Exists('..\nuget.config')" />
<None Include="..\Jenkinsfile" Condition="Exists('..\Jenkinsfile')" />
<None Include="..\appveyor.yml" Condition="Exists('..\appveyor.yml')" />
<None Include="..\.travis.yml" Condition="Exists('..\.travis.yml')" />
<None Include="..\GitVersion.yml" Condition="Exists('..\GitVersion.yml')" />
<Compile Remove="Numerge/**/*.*" />
<Compile Include="Numerge/Numerge/**/*.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="$(NuGetPackageRoot)sourcelink/1.1.0/tools/pdbstr.exe"></EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Remove="il-repack\ILRepack\Application.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="Numerge\Numerge.Console\" />
</ItemGroup>
</Project>

1
nukebuild/il-repack

@ -0,0 +1 @@
Subproject commit 892f079ea8cb0c178f0a68f53a7a7eac13acdda9

3
samples/ControlCatalog/MainView.xaml

@ -19,6 +19,9 @@
<TabItem Header="Acrylic">
<pages:AcrylicPage />
</TabItem>
<TabItem Header="AdornerLayer">
<pages:AdornerLayerPage />
</TabItem>
<TabItem Header="AutoCompleteBox">
<pages:AutoCompleteBoxPage />
</TabItem>

5
samples/ControlCatalog/MainView.xaml.cs

@ -2,10 +2,10 @@ using System;
using System.Collections;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Themes.Fluent;
using ControlCatalog.Models;
using ControlCatalog.Pages;
@ -20,7 +20,7 @@ namespace ControlCatalog
var sideBar = this.Get<TabControl>("Sidebar");
if (AvaloniaLocator.Current?.GetService<IRuntimePlatform>()?.GetRuntimeInfo().IsDesktop == true)
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
{
var tabItems = (sideBar.Items as IList);
tabItems?.Add(new TabItem()
@ -28,7 +28,6 @@ namespace ControlCatalog
Header = "Screens",
Content = new ScreenPage()
});
}
var themes = this.Get<ComboBox>("Themes");

66
samples/ControlCatalog/Pages/AdornerLayerPage.xaml

@ -0,0 +1,66 @@
<UserControl x:Class="ControlCatalog.Pages.AdornerLayerPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="800"
d:DesignWidth="400"
mc:Ignorable="d">
<DockPanel>
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto" Margin="16" DockPanel.Dock="Top">
<TextBlock Grid.Column="0" Grid.Row="0">Rotation</TextBlock>
<Slider Name="rotation" Maximum="360" Grid.Column="1" Grid.Row="0"/>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" DockPanel.Dock="Top">
<Button Name="AddAdorner"
Click="AddAdorner_OnClick"
Content="Add Adorner"
Margin="6" />
<Button Name="RemoveAdorner"
Click="RemoveAdorner_OnClick"
Content="Remove Adorner"
Margin="6" />
</StackPanel>
<Grid ColumnDefinitions="24,Auto,24"
RowDefinitions="24,Auto,24"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border Background="{DynamicResource SystemAccentColor}" Grid.Column="1" Grid.Row="0"/>
<Border Background="{DynamicResource SystemAccentColor}" Grid.Column="0" Grid.Row="1"/>
<Border Background="{DynamicResource SystemAccentColor}" Grid.Column="2" Grid.Row="1"/>
<Border Background="{DynamicResource SystemAccentColor}" Grid.Column="1" Grid.Row="2"/>
<LayoutTransformControl Name="layoutTransform" Grid.Column="1" Grid.Row="1">
<LayoutTransformControl.LayoutTransform>
<RotateTransform Angle="{Binding #rotation.Value}"/>
</LayoutTransformControl.LayoutTransform>
<Button Name="AdornerButton"
Content="Adorned Button"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
VerticalContentAlignment="Center" VerticalAlignment="Stretch"
Width="200" Height="42">
<AdornerLayer.Adorner>
<Canvas x:Name="AdornerCanvas"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Cyan"
IsHitTestVisible="False"
Opacity="0.3"
IsVisible="True">
<Line StartPoint="-10000,0" EndPoint="10000,0" Stroke="Cyan" StrokeThickness="1" />
<Line StartPoint="-10000,42" EndPoint="10000,42" Stroke="Cyan" StrokeThickness="1" />
<Line StartPoint="0,-10000" EndPoint="0,10000" Stroke="Cyan" StrokeThickness="1" />
<Line StartPoint="200,-10000" EndPoint="200,10000" Stroke="Cyan" StrokeThickness="1" />
</Canvas>
</AdornerLayer.Adorner>
</Button>
</LayoutTransformControl>
</Grid>
</DockPanel>
</UserControl>

48
samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs

@ -0,0 +1,48 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
{
public class AdornerLayerPage : UserControl
{
private Control? _adorner;
public AdornerLayerPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void RemoveAdorner_OnClick(object? sender, RoutedEventArgs e)
{
var adornerButton = this.FindControl<Button>("AdornerButton");
if (adornerButton is { })
{
var adorner = AdornerLayer.GetAdorner(adornerButton);
if (adorner is { })
{
_adorner = adorner;
}
AdornerLayer.SetAdorner(adornerButton, null);
}
}
private void AddAdorner_OnClick(object? sender, RoutedEventArgs e)
{
var adornerButton = this.FindControl<Button>("AdornerButton");
if (adornerButton is { })
{
if (_adorner is { })
{
AdornerLayer.SetAdorner(adornerButton, _adorner);
}
}
}
}
}

12
samples/ControlCatalog/Pages/ScreenPage.cs

@ -2,10 +2,10 @@ using System;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace ControlCatalog.Pages
{
@ -18,8 +18,10 @@ namespace ControlCatalog.Pages
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
Window w = (Window)VisualRoot!;
w.PositionChanged += (sender, args) => InvalidateVisual();
if(VisualRoot is Window w)
{
w.PositionChanged += (_, _) => InvalidateVisual();
}
}
public override void Render(DrawingContext context)
@ -27,7 +29,7 @@ namespace ControlCatalog.Pages
base.Render(context);
if (!(VisualRoot is Window w))
{
return;
return;
}
var screens = w.Screens.All;
var scaling = ((IRenderRoot)w).RenderScaling;
@ -40,7 +42,7 @@ namespace ControlCatalog.Pages
if (screen.Bounds.X / 10f < _leftMost)
{
_leftMost = screen.Bounds.X / 10f;
InvalidateVisual();
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
return;
}

5
samples/RenderDemo/Pages/FormattedTextPage.axaml.cs

@ -3,6 +3,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Immutable;
namespace RenderDemo.Pages
{
@ -59,6 +60,10 @@ namespace RenderDemo.Pages
var geometry = formattedText.BuildGeometry(new Point(10 + formattedText.Width + 10, 0));
context.DrawGeometry(gradient, null, geometry);
var highlightGeometry = formattedText.BuildHighlightGeometry(new Point(10 + formattedText.Width + 10, 0));
context.DrawGeometry(null, new ImmutablePen(gradient.ToImmutable(), 2), highlightGeometry);
}
}
}

1
samples/Sandbox/MainWindow.axaml

@ -1,5 +1,4 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="Sandbox.MainWindow">
<TextBox />
</Window>

35
src/Avalonia.Base/Logging/LogArea.cs

@ -8,51 +8,66 @@ namespace Avalonia.Logging
/// <summary>
/// The log event comes from the property system.
/// </summary>
public const string Property = "Property";
public const string Property = nameof(Property);
/// <summary>
/// The log event comes from the binding system.
/// </summary>
public const string Binding = "Binding";
public const string Binding = nameof(Binding);
/// <summary>
/// The log event comes from the animations system.
/// </summary>
public const string Animations = "Animations";
public const string Animations = nameof(Animations);
/// <summary>
/// The log event comes from the visual system.
/// </summary>
public const string Visual = "Visual";
public const string Visual = nameof(Visual);
/// <summary>
/// The log event comes from the layout system.
/// </summary>
public const string Layout = "Layout";
public const string Layout = nameof(Layout);
/// <summary>
/// The log event comes from the control system.
/// </summary>
public const string Control = "Control";
public const string Control = nameof(Control);
/// <summary>
/// The log event comes from Win32Platform.
/// The log event comes from Win32 Platform.
/// </summary>
public const string Win32Platform = nameof(Win32Platform);
/// <summary>
/// The log event comes from X11Platform.
/// The log event comes from X11 Platform.
/// </summary>
public const string X11Platform = nameof(X11Platform);
/// <summary>
/// The log event comes from AndroidPlatform.
/// The log event comes from Android Platform.
/// </summary>
public const string AndroidPlatform = nameof(AndroidPlatform);
/// <summary>
/// The log event comes from IOSPlatform.
/// The log event comes from iOS Platform.
/// </summary>
public const string IOSPlatform = nameof(IOSPlatform);
/// <summary>
/// The log event comes from LinuxFramebuffer Platform
/// </summary>
public const string LinuxFramebufferPlatform = nameof(LinuxFramebufferPlatform);
/// <summary>
/// The log event comes from FreeDesktop Platform
/// </summary>
public const string FreeDesktopPlatform = nameof(FreeDesktopPlatform);
/// <summary>
/// The log event comes from macOS Platform
/// </summary>
public const string macOSPlatform = nameof(macOSPlatform);
}
}

106
src/Avalonia.Base/Media/FormattedText.cs

@ -1,8 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
@ -654,14 +656,16 @@ namespace Avalonia.Media
// line break before _currentLine, needed in case we have to reformat it with collapsing symbol
private TextLineBreak? _previousLineBreak;
private int _position;
private int _length;
internal LineEnumerator(FormattedText text)
{
_previousHeight = 0;
Length = 0;
_length = 0;
_previousLineBreak = null;
Position = 0;
_position = 0;
_lineCount = 0;
_totalHeight = 0;
Current = null;
@ -678,9 +682,17 @@ namespace Avalonia.Media
_nextLine = null;
}
private int Position { get; set; }
public int Position
{
get => _position;
private set => _position = value;
}
private int Length { get; set; }
public int Length
{
get => _length;
private set => _length = value;
}
/// <summary>
/// Gets the current text line in the collection
@ -1292,6 +1304,92 @@ namespace Avalonia.Media
return accumulatedGeometry;
}
/// <summary>
/// Builds a highlight geometry object.
/// </summary>
/// <param name="origin">The origin of the highlight region</param>
/// <returns>Geometry that surrounds the text.</returns>
public Geometry? BuildHighlightGeometry(Point origin)
{
return BuildHighlightGeometry(origin, 0, _text.Length);
}
/// <summary>
/// Builds a highlight geometry object for a given character range.
/// </summary>
/// <param name="origin">The origin of the highlight region.</param>
/// <param name="startIndex">The start index of initial character the bounds should be obtained for.</param>
/// <param name="count">The number of characters the bounds should be obtained for.</param>
/// <returns>Geometry that surrounds the specified character range.</returns>
public Geometry? BuildHighlightGeometry(Point origin, int startIndex, int count)
{
ValidateRange(startIndex, count);
Geometry? accumulatedBounds = null;
using (var enumerator = GetEnumerator())
{
var lineOrigin = origin;
while (enumerator.MoveNext())
{
var currentLine = enumerator.Current!;
int x0 = Math.Max(enumerator.Position, startIndex);
int x1 = Math.Min(enumerator.Position + enumerator.Length, startIndex + count);
// check if this line is intersects with the specified character range
if (x0 < x1)
{
var highlightBounds = currentLine.GetTextBounds(x0,x1 - x0);
if (highlightBounds != null)
{
foreach (var bound in highlightBounds)
{
var rect = bound.Rectangle;
if (FlowDirection == FlowDirection.RightToLeft)
{
// Convert logical units (which extend leftward from the right edge
// of the paragraph) to physical units.
//
// Note that since rect is in logical units, rect.Right corresponds to
// the visual *left* edge of the rectangle in the RTL case. Specifically,
// is the distance leftward from the right edge of the formatting rectangle
// whose width is the paragraph width passed to FormatLine.
//
rect = rect.WithX(enumerator.CurrentParagraphWidth - rect.Right);
}
rect = new Rect(new Point(rect.X + lineOrigin.X, rect.Y + lineOrigin.Y), rect.Size);
RectangleGeometry rectangleGeometry = new RectangleGeometry(rect);
if (accumulatedBounds == null)
{
accumulatedBounds = rectangleGeometry;
}
else
{
accumulatedBounds = Geometry.Combine(accumulatedBounds, rectangleGeometry, GeometryCombineMode.Union);
}
}
}
}
AdvanceLineOrigin(ref lineOrigin, currentLine);
}
}
if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsEmpty)
{
return null;
}
return accumulatedBounds;
}
/// <summary>
/// Draws the text object
/// </summary>

13
src/Avalonia.Base/Media/Geometry.cs

@ -185,5 +185,18 @@ namespace Avalonia.Media
var control = e.Sender as Geometry;
control?.InvalidateGeometry();
}
/// <summary>
/// Combines the two geometries using the specified <see cref="GeometryCombineMode"/> and applies the specified transform to the resulting geometry.
/// </summary>
/// <param name="geometry1">The first geometry to combine.</param>
/// <param name="geometry2">The second geometry to combine.</param>
/// <param name="combineMode">One of the enumeration values that specifies how the geometries are combined.</param>
/// <param name="transform">A transformation to apply to the combined geometry, or <c>null</c>.</param>
/// <returns></returns>
public static Geometry Combine(Geometry geometry1, RectangleGeometry geometry2, GeometryCombineMode combineMode, Transform? transform = null)
{
return new CombinedGeometry(combineMode, geometry1, geometry2, transform);
}
}
}

11
src/Avalonia.Base/Media/GlyphRun.cs

@ -789,14 +789,15 @@ namespace Avalonia.Media
var clusterLength = 1;
while (i - 1 >= 0)
var j = i;
while (j - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
var nextCluster = GlyphClusters[--j];
if (currentCluster == nextCluster)
{
clusterLength++;
i--;
clusterLength++;
continue;
}
@ -811,7 +812,7 @@ namespace Avalonia.Media
trailingWhitespaceLength += clusterLength;
glyphCount++;
glyphCount += clusterLength;
}
}

253
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
var collapsingProperties = collapsingPropertiesList[0];
if(collapsingProperties is null)
if (collapsingProperties is null)
{
return this;
}
@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = _textRuns[i];
if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
{
var rightToLeftIndex = i;
currentPosition += currentRun.TextSourceLength;
@ -213,14 +213,14 @@ namespace Avalonia.Media.TextFormatting
for (var j = i; i <= rightToLeftIndex; j++)
{
if(j > _textRuns.Count - 1)
if (j > _textRuns.Count - 1)
{
break;
}
currentRun = _textRuns[j];
if(currentDistance + currentRun.Size.Width <= distance)
if (currentDistance + currentRun.Size.Width <= distance)
{
currentDistance += currentRun.Size.Width;
currentPosition -= currentRun.TextSourceLength;
@ -266,10 +266,6 @@ namespace Avalonia.Media.TextFormatting
{
offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
}
//else
//{
// offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length);
//}
characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
@ -326,11 +322,11 @@ namespace Avalonia.Media.TextFormatting
continue;
}
break;
}
if(i > index)
if (i > index)
{
while (i >= index)
{
@ -354,7 +350,7 @@ namespace Avalonia.Media.TextFormatting
}
}
if (currentPosition + currentRun.TextSourceLength >= characterIndex &&
if (currentPosition + currentRun.TextSourceLength >= characterIndex &&
TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _))
{
return Math.Max(0, currentDistance + distance);
@ -534,6 +530,8 @@ namespace Avalonia.Media.TextFormatting
double currentWidth = 0;
var currentRect = Rect.Empty;
TextRunBounds lastRunBounds = default;
for (var index = 0; index < TextRuns.Count; index++)
{
if (TextRuns[index] is not DrawableTextRun currentRun)
@ -543,53 +541,93 @@ namespace Avalonia.Media.TextFormatting
var characterLength = 0;
var endX = startX;
var runWidth = 0.0;
TextRunBounds? currentRunBounds = null;
var currentShapedRun = currentRun as ShapedTextCharacters;
TextRunBounds currentRunBounds;
double combinedWidth;
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
continue;
}
if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight)
{
var rightToLeftIndex = index;
startX += currentShapedRun.Size.Width;
var rightToLeftWidth = currentShapedRun.Size.Width;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
{
var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters;
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{
break;
}
startX += nextShapedRun.Size.Width;
rightToLeftIndex++;
rightToLeftWidth += nextShapedRun.Size.Width;
if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength)
{
break;
}
currentShapedRun = nextShapedRun;
}
if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds))
startX = startX + rightToLeftWidth;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
remainingLength -= currentRunBounds.Length;
currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
endX = currentRunBounds.Rectangle.Right;
startX = currentRunBounds.Rectangle.Left;
var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
for (int i = rightToLeftIndex - 1; i >= index; i--)
{
startX = currentRunBounds!.Rectangle.Left;
endX = currentRunBounds.Rectangle.Right;
currentShapedRun = TextRuns[i] as ShapedTextCharacters;
if(currentShapedRun == null)
{
continue;
}
runWidth = currentRunBounds.Rectangle.Width;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
rightToLeftRunBounds.Insert(0, currentRunBounds);
remainingLength -= currentRunBounds.Length;
startX = currentRunBounds.Rectangle.Left;
currentPosition += currentRunBounds.Length;
}
combinedWidth = endX - startX;
currentRect = new Rect(startX, 0, combinedWidth, Height);
currentDirection = FlowDirection.RightToLeft;
if (!MathUtilities.IsZero(combinedWidth))
{
result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
}
startX = endX;
}
else
{
if (currentShapedRun != null)
{
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
continue;
}
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
currentPosition += offset;
@ -665,43 +703,46 @@ namespace Avalonia.Media.TextFormatting
characterLength = NewLineLength;
}
runWidth = endX - startX;
currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
combinedWidth = endX - startX;
currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
currentPosition += characterLength;
remainingLength -= characterLength;
}
if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0)
{
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
startX = endX;
if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
{
currentRect = currentRect.WithWidth(currentWidth + runWidth);
if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
{
currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
var textBounds = result[result.Count - 1];
var textBounds = result[result.Count - 1];
textBounds.Rectangle = currentRect;
textBounds.Rectangle = currentRect;
textBounds.TextRunBounds.Add(currentRunBounds!);
}
else
{
currentRect = currentRunBounds!.Rectangle;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
}
lastRunBounds = currentRunBounds;
}
currentWidth += runWidth;
currentWidth += combinedWidth;
if (remainingLength <= 0 || currentPosition >= characterIndex)
{
break;
}
startX = endX;
lastDirection = currentDirection;
}
@ -856,105 +897,45 @@ namespace Avalonia.Media.TextFormatting
return result;
}
private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds)
private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextCharacters currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength)
{
textRunBounds = null;
var startX = endX;
for (var index = runIndex; index >= 0; index--)
{
if (TextRuns[index] is not DrawableTextRun currentRun)
{
continue;
}
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
startX -= currentRun.Size.Width;
currentPosition += offset;
currentPosition += currentRun.TextSourceLength;
var startIndex = currentRun.Text.Start + offset;
continue;
}
double startOffset;
double endOffset;
var characterLength = 0;
var endX = startX;
if (currentRun is ShapedTextCharacters currentShapedRun)
{
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
currentPosition += offset;
var startIndex = currentRun.Text.Start + offset;
double startOffset;
double endOffset;
if (currentShapedRun.ShapedBuffer.IsLeftToRight)
{
if (currentPosition < startIndex)
{
startOffset = endOffset = 0;
}
else
{
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
}
}
else
{
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
}
startX -= currentRun.Size.Width - startOffset;
endX -= currentRun.Size.Width - endOffset;
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
}
else
{
if (currentPosition + currentRun.TextSourceLength <= characterIndex)
{
endX -= currentRun.Size.Width;
}
startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
if (currentPosition < firstTextSourceIndex)
{
startX -= currentRun.Size.Width;
startX -= currentRun.Size.Width - startOffset;
endX -= currentRun.Size.Width - endOffset;
characterLength = currentRun.TextSourceLength;
}
}
var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
if (endX < startX)
{
(endX, startX) = (startX, endX);
}
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
//Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
{
characterLength = NewLineLength;
}
var runWidth = endX - startX;
remainingLength -= characterLength;
currentPosition += characterLength;
textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
if (endX < startX)
{
(endX, startX) = (startX, endX);
}
return true;
//Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
{
characterLength = NewLineLength;
}
return false;
var runWidth = endX - startX;
return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
@ -1536,7 +1517,7 @@ namespace Avalonia.Media.TextFormatting
var textAlignment = _paragraphProperties.TextAlignment;
var paragraphFlowDirection = _paragraphProperties.FlowDirection;
if(textAlignment == TextAlignment.Justify)
if (textAlignment == TextAlignment.Justify)
{
textAlignment = TextAlignment.Start;
}

2
src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs

@ -3,7 +3,7 @@
/// <summary>
/// The bounding rectangle of text run
/// </summary>
public sealed class TextRunBounds
public readonly struct TextRunBounds
{
/// <summary>
/// Constructing TextRunBounds

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

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

1
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -30,4 +30,5 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Web.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Web, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

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

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

2
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -105,7 +105,7 @@
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\**\obj\**\*.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" />
<PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
<PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
</ItemGroup>

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

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

359
src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs

@ -0,0 +1,359 @@
using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
/// <summary>
/// Implements a reduced version of the 2014 Material Design color palette.
/// </summary>
/// <remarks>
/// This palette is based on the one outlined here:
///
/// https://material.io/design/color/the-color-system.html#tools-for-picking-colors
///
/// In order to make the palette uniform and rectangular the following
/// alterations were made:
///
/// 1. The A100-A700 shades of each color are excluded.
/// These shades do not exist for all colors (brown/gray).
/// 2. Black/White are stand-alone and are also excluded.
///
/// </remarks>
public class MaterialColorPalette : IColorPalette
{
// See: https://material.io/design/color/the-color-system.html#tools-for-picking-colors
// This is a reduced palette for uniformity
private static Color[,]? _colorChart = null;
private static int _colorChartColorCount = 0;
private static int _colorChartShadeCount = 0;
private static object _colorChartMutex = new object();
/// <summary>
/// Initializes all color chart colors.
/// </summary>
/// <remarks>
/// This is pulled out separately to lazy load for performance.
/// If no material color palette is ever used, no colors will be created.
/// </remarks>
private void InitColorChart()
{
lock (_colorChartMutex)
{
_colorChart = new Color[,]
{
// Red
{
Color.FromArgb(0xFF, 0xFF, 0xEB, 0xEE),
Color.FromArgb(0xFF, 0xFF, 0xCD, 0xD2),
Color.FromArgb(0xFF, 0xEF, 0x9A, 0x9A),
Color.FromArgb(0xFF, 0xE5, 0x73, 0x73),
Color.FromArgb(0xFF, 0xEF, 0x53, 0x50),
Color.FromArgb(0xFF, 0xF4, 0x43, 0x36),
Color.FromArgb(0xFF, 0xE5, 0x39, 0x35),
Color.FromArgb(0xFF, 0xD3, 0x2F, 0x2F),
Color.FromArgb(0xFF, 0xC6, 0x28, 0x28),
Color.FromArgb(0xFF, 0xB7, 0x1C, 0x1C),
},
// Pink
{
Color.FromArgb(0xFF, 0xFC, 0xE4, 0xEC),
Color.FromArgb(0xFF, 0xF8, 0xBB, 0xD0),
Color.FromArgb(0xFF, 0xF4, 0x8F, 0xB1),
Color.FromArgb(0xFF, 0xF0, 0x62, 0x92),
Color.FromArgb(0xFF, 0xEC, 0x40, 0x7A),
Color.FromArgb(0xFF, 0xE9, 0x1E, 0x63),
Color.FromArgb(0xFF, 0xD8, 0x1B, 0x60),
Color.FromArgb(0xFF, 0xC2, 0x18, 0x5B),
Color.FromArgb(0xFF, 0xAD, 0x14, 0x57),
Color.FromArgb(0xFF, 0x88, 0x0E, 0x4F),
},
// Purple
{
Color.FromArgb(0xFF, 0xF3, 0xE5, 0xF5),
Color.FromArgb(0xFF, 0xE1, 0xBE, 0xE7),
Color.FromArgb(0xFF, 0xCE, 0x93, 0xD8),
Color.FromArgb(0xFF, 0xBA, 0x68, 0xC8),
Color.FromArgb(0xFF, 0xAB, 0x47, 0xBC),
Color.FromArgb(0xFF, 0x9C, 0x27, 0xB0),
Color.FromArgb(0xFF, 0x8E, 0x24, 0xAA),
Color.FromArgb(0xFF, 0x7B, 0x1F, 0xA2),
Color.FromArgb(0xFF, 0x6A, 0x1B, 0x9A),
Color.FromArgb(0xFF, 0x4A, 0x14, 0x8C),
},
// Deep Purple
{
Color.FromArgb(0xFF, 0xED, 0xE7, 0xF6),
Color.FromArgb(0xFF, 0xD1, 0xC4, 0xE9),
Color.FromArgb(0xFF, 0xB3, 0x9D, 0xDB),
Color.FromArgb(0xFF, 0x95, 0x75, 0xCD),
Color.FromArgb(0xFF, 0x7E, 0x57, 0xC2),
Color.FromArgb(0xFF, 0x67, 0x3A, 0xB7),
Color.FromArgb(0xFF, 0x5E, 0x35, 0xB1),
Color.FromArgb(0xFF, 0x51, 0x2D, 0xA8),
Color.FromArgb(0xFF, 0x45, 0x27, 0xA0),
Color.FromArgb(0xFF, 0x31, 0x1B, 0x92),
},
// Indigo
{
Color.FromArgb(0xFF, 0xE8, 0xEA, 0xF6),
Color.FromArgb(0xFF, 0xC5, 0xCA, 0xE9),
Color.FromArgb(0xFF, 0x9F, 0xA8, 0xDA),
Color.FromArgb(0xFF, 0x79, 0x86, 0xCB),
Color.FromArgb(0xFF, 0x5C, 0x6B, 0xC0),
Color.FromArgb(0xFF, 0x3F, 0x51, 0xB5),
Color.FromArgb(0xFF, 0x39, 0x49, 0xAB),
Color.FromArgb(0xFF, 0x30, 0x3F, 0x9F),
Color.FromArgb(0xFF, 0x28, 0x35, 0x93),
Color.FromArgb(0xFF, 0x1A, 0x23, 0x7E),
},
// Blue
{
Color.FromArgb(0xFF, 0xE3, 0xF2, 0xFD),
Color.FromArgb(0xFF, 0xBB, 0xDE, 0xFB),
Color.FromArgb(0xFF, 0x90, 0xCA, 0xF9),
Color.FromArgb(0xFF, 0x64, 0xB5, 0xF6),
Color.FromArgb(0xFF, 0x42, 0xA5, 0xF5),
Color.FromArgb(0xFF, 0x21, 0x96, 0xF3),
Color.FromArgb(0xFF, 0x1E, 0x88, 0xE5),
Color.FromArgb(0xFF, 0x19, 0x76, 0xD2),
Color.FromArgb(0xFF, 0x15, 0x65, 0xC0),
Color.FromArgb(0xFF, 0x0D, 0x47, 0xA1),
},
// Light Blue
{
Color.FromArgb(0xFF, 0xE1, 0xF5, 0xFE),
Color.FromArgb(0xFF, 0xB3, 0xE5, 0xFC),
Color.FromArgb(0xFF, 0x81, 0xD4, 0xFA),
Color.FromArgb(0xFF, 0x4F, 0xC3, 0xF7),
Color.FromArgb(0xFF, 0x29, 0xB6, 0xF6),
Color.FromArgb(0xFF, 0x03, 0xA9, 0xF4),
Color.FromArgb(0xFF, 0x03, 0x9B, 0xE5),
Color.FromArgb(0xFF, 0x02, 0x88, 0xD1),
Color.FromArgb(0xFF, 0x02, 0x77, 0xBD),
Color.FromArgb(0xFF, 0x01, 0x57, 0x9B),
},
// Cyan
{
Color.FromArgb(0xFF, 0xE0, 0xF7, 0xFA),
Color.FromArgb(0xFF, 0xB2, 0xEB, 0xF2),
Color.FromArgb(0xFF, 0x80, 0xDE, 0xEA),
Color.FromArgb(0xFF, 0x4D, 0xD0, 0xE1),
Color.FromArgb(0xFF, 0x26, 0xC6, 0xDA),
Color.FromArgb(0xFF, 0x00, 0xBC, 0xD4),
Color.FromArgb(0xFF, 0x00, 0xAC, 0xC1),
Color.FromArgb(0xFF, 0x00, 0x97, 0xA7),
Color.FromArgb(0xFF, 0x00, 0x83, 0x8F),
Color.FromArgb(0xFF, 0x00, 0x60, 0x64),
},
// Teal
{
Color.FromArgb(0xFF, 0xE0, 0xF2, 0xF1),
Color.FromArgb(0xFF, 0xB2, 0xDF, 0xDB),
Color.FromArgb(0xFF, 0x80, 0xCB, 0xC4),
Color.FromArgb(0xFF, 0x4D, 0xB6, 0xAC),
Color.FromArgb(0xFF, 0x26, 0xA6, 0x9A),
Color.FromArgb(0xFF, 0x00, 0x96, 0x88),
Color.FromArgb(0xFF, 0x00, 0x89, 0x7B),
Color.FromArgb(0xFF, 0x00, 0x79, 0x6B),
Color.FromArgb(0xFF, 0x00, 0x69, 0x5C),
Color.FromArgb(0xFF, 0x00, 0x4D, 0x40),
},
// Green
{
Color.FromArgb(0xFF, 0xE8, 0xF5, 0xE9),
Color.FromArgb(0xFF, 0xC8, 0xE6, 0xC9),
Color.FromArgb(0xFF, 0xA5, 0xD6, 0xA7),
Color.FromArgb(0xFF, 0x81, 0xC7, 0x84),
Color.FromArgb(0xFF, 0x66, 0xBB, 0x6A),
Color.FromArgb(0xFF, 0x4C, 0xAF, 0x50),
Color.FromArgb(0xFF, 0x43, 0xA0, 0x47),
Color.FromArgb(0xFF, 0x38, 0x8E, 0x3C),
Color.FromArgb(0xFF, 0x2E, 0x7D, 0x32),
Color.FromArgb(0xFF, 0x1B, 0x5E, 0x20),
},
// Light Green
{
Color.FromArgb(0xFF, 0xF1, 0xF8, 0xE9),
Color.FromArgb(0xFF, 0xDC, 0xED, 0xC8),
Color.FromArgb(0xFF, 0xC5, 0xE1, 0xA5),
Color.FromArgb(0xFF, 0xAE, 0xD5, 0x81),
Color.FromArgb(0xFF, 0x9C, 0xCC, 0x65),
Color.FromArgb(0xFF, 0x8B, 0xC3, 0x4A),
Color.FromArgb(0xFF, 0x7C, 0xB3, 0x42),
Color.FromArgb(0xFF, 0x68, 0x9F, 0x38),
Color.FromArgb(0xFF, 0x55, 0x8B, 0x2F),
Color.FromArgb(0xFF, 0x33, 0x69, 0x1E),
},
// Lime
{
Color.FromArgb(0xFF, 0xF9, 0xFB, 0xE7),
Color.FromArgb(0xFF, 0xF0, 0xF4, 0xC3),
Color.FromArgb(0xFF, 0xE6, 0xEE, 0x9C),
Color.FromArgb(0xFF, 0xDC, 0xE7, 0x75),
Color.FromArgb(0xFF, 0xD4, 0xE1, 0x57),
Color.FromArgb(0xFF, 0xCD, 0xDC, 0x39),
Color.FromArgb(0xFF, 0xC0, 0xCA, 0x33),
Color.FromArgb(0xFF, 0xAF, 0xB4, 0x2B),
Color.FromArgb(0xFF, 0x9E, 0x9D, 0x24),
Color.FromArgb(0xFF, 0x82, 0x77, 0x17),
},
// Yellow
{
Color.FromArgb(0xFF, 0xFF, 0xFD, 0xE7),
Color.FromArgb(0xFF, 0xFF, 0xF9, 0xC4),
Color.FromArgb(0xFF, 0xFF, 0xF5, 0x9D),
Color.FromArgb(0xFF, 0xFF, 0xF1, 0x76),
Color.FromArgb(0xFF, 0xFF, 0xEE, 0x58),
Color.FromArgb(0xFF, 0xFF, 0xEB, 0x3B),
Color.FromArgb(0xFF, 0xFD, 0xD8, 0x35),
Color.FromArgb(0xFF, 0xFB, 0xC0, 0x2D),
Color.FromArgb(0xFF, 0xF9, 0xA8, 0x25),
Color.FromArgb(0xFF, 0xF5, 0x7F, 0x17),
},
// Amber
{
Color.FromArgb(0xFF, 0xFF, 0xF8, 0xE1),
Color.FromArgb(0xFF, 0xFF, 0xEC, 0xB3),
Color.FromArgb(0xFF, 0xFF, 0xE0, 0x82),
Color.FromArgb(0xFF, 0xFF, 0xD5, 0x4F),
Color.FromArgb(0xFF, 0xFF, 0xCA, 0x28),
Color.FromArgb(0xFF, 0xFF, 0xC1, 0x07),
Color.FromArgb(0xFF, 0xFF, 0xB3, 0x00),
Color.FromArgb(0xFF, 0xFF, 0xA0, 0x00),
Color.FromArgb(0xFF, 0xFF, 0x8F, 0x00),
Color.FromArgb(0xFF, 0xFF, 0x6F, 0x00),
},
// Orange
{
Color.FromArgb(0xFF, 0xFF, 0xF3, 0xE0),
Color.FromArgb(0xFF, 0xFF, 0xE0, 0xB2),
Color.FromArgb(0xFF, 0xFF, 0xCC, 0x80),
Color.FromArgb(0xFF, 0xFF, 0xB7, 0x4D),
Color.FromArgb(0xFF, 0xFF, 0xA7, 0x26),
Color.FromArgb(0xFF, 0xFF, 0x98, 0x00),
Color.FromArgb(0xFF, 0xFB, 0x8C, 0x00),
Color.FromArgb(0xFF, 0xF5, 0x7C, 0x00),
Color.FromArgb(0xFF, 0xEF, 0x6C, 0x00),
Color.FromArgb(0xFF, 0xE6, 0x51, 0x00),
},
// Deep Orange
{
Color.FromArgb(0xFF, 0xFB, 0xE9, 0xE7),
Color.FromArgb(0xFF, 0xFF, 0xCC, 0xBC),
Color.FromArgb(0xFF, 0xFF, 0xAB, 0x91),
Color.FromArgb(0xFF, 0xFF, 0x8A, 0x65),
Color.FromArgb(0xFF, 0xFF, 0x70, 0x43),
Color.FromArgb(0xFF, 0xFF, 0x57, 0x22),
Color.FromArgb(0xFF, 0xF4, 0x51, 0x1E),
Color.FromArgb(0xFF, 0xE6, 0x4A, 0x19),
Color.FromArgb(0xFF, 0xD8, 0x43, 0x15),
Color.FromArgb(0xFF, 0xBF, 0x36, 0x0C),
},
// Brown
{
Color.FromArgb(0xFF, 0xEF, 0xEB, 0xE9),
Color.FromArgb(0xFF, 0xD7, 0xCC, 0xC8),
Color.FromArgb(0xFF, 0xBC, 0xAA, 0xA4),
Color.FromArgb(0xFF, 0xA1, 0x88, 0x7F),
Color.FromArgb(0xFF, 0x8D, 0x6E, 0x63),
Color.FromArgb(0xFF, 0x79, 0x55, 0x48),
Color.FromArgb(0xFF, 0x6D, 0x4C, 0x41),
Color.FromArgb(0xFF, 0x5D, 0x40, 0x37),
Color.FromArgb(0xFF, 0x4E, 0x34, 0x2E),
Color.FromArgb(0xFF, 0x3E, 0x27, 0x23),
},
// Gray
{
Color.FromArgb(0xFF, 0xFA, 0xFA, 0xFA),
Color.FromArgb(0xFF, 0xF5, 0xF5, 0xF5),
Color.FromArgb(0xFF, 0xEE, 0xEE, 0xEE),
Color.FromArgb(0xFF, 0xE0, 0xE0, 0xE0),
Color.FromArgb(0xFF, 0xBD, 0xBD, 0xBD),
Color.FromArgb(0xFF, 0x9E, 0x9E, 0x9E),
Color.FromArgb(0xFF, 0x75, 0x75, 0x75),
Color.FromArgb(0xFF, 0x61, 0x61, 0x61),
Color.FromArgb(0xFF, 0x42, 0x42, 0x42),
Color.FromArgb(0xFF, 0x21, 0x21, 0x21),
},
// Blue Gray
{
Color.FromArgb(0xFF, 0xEC, 0xEF, 0xF1),
Color.FromArgb(0xFF, 0xCF, 0xD8, 0xDC),
Color.FromArgb(0xFF, 0xB0, 0xBE, 0xC5),
Color.FromArgb(0xFF, 0x90, 0xA4, 0xAE),
Color.FromArgb(0xFF, 0x78, 0x90, 0x9C),
Color.FromArgb(0xFF, 0x60, 0x7D, 0x8B),
Color.FromArgb(0xFF, 0x54, 0x6E, 0x7A),
Color.FromArgb(0xFF, 0x45, 0x5A, 0x64),
Color.FromArgb(0xFF, 0x37, 0x47, 0x4F),
Color.FromArgb(0xFF, 0x26, 0x32, 0x38),
},
};
_colorChartColorCount = _colorChart.GetLength(0);
_colorChartShadeCount = _colorChart.GetLength(1);
}
return;
}
/// <inheritdoc/>
public int ColorCount
{
get
{
if (_colorChart == null)
{
InitColorChart();
}
return _colorChartColorCount;
}
}
/// <inheritdoc/>
public int ShadeCount
{
get
{
if (_colorChart == null)
{
InitColorChart();
}
return _colorChartShadeCount;
}
}
/// <inheritdoc/>
public Color GetColor(int colorIndex, int shadeIndex)
{
if (_colorChart == null)
{
InitColorChart();
}
return _colorChart![
MathUtilities.Clamp(colorIndex, 0, _colorChartColorCount - 1),
MathUtilities.Clamp(shadeIndex, 0, _colorChartShadeCount - 1)];
}
}
}

28
src/Avalonia.Controls/ComboBox.cs

@ -21,8 +21,11 @@ namespace Avalonia.Controls
/// A drop-down list control.
/// </summary>
[TemplatePart("PART_Popup", typeof(Popup))]
[PseudoClasses(pcDropdownOpen, pcPressed)]
public class ComboBox : SelectingItemsControl
{
public const string pcDropdownOpen = ":dropdownopen";
public const string pcPressed = ":pressed";
/// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
/// </summary>
@ -95,6 +98,7 @@ namespace Avalonia.Controls
SelectedItemProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.SelectedItemChanged(e));
KeyDownEvent.AddClassHandler<ComboBox>((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel);
IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true);
IsDropDownOpenProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.DropdownChanged(e));
}
/// <summary>
@ -267,6 +271,20 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if(!e.Handled && e.Source is IVisual source)
{
if (_popup?.IsInsidePopup(source) == true)
{
return;
}
}
PseudoClasses.Set(pcPressed, true);
}
/// <inheritdoc/>
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
@ -286,10 +304,12 @@ namespace Avalonia.Controls
e.Handled = true;
}
}
PseudoClasses.Set(pcPressed, false);
base.OnPointerReleased(e);
}
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
@ -470,5 +490,11 @@ namespace Avalonia.Controls
MoveSelection(NavigationDirection.Previous, WrapSelection);
}
}
private void DropdownChanged(AvaloniaPropertyChangedEventArgs e)
{
bool newValue = e.GetNewValue<bool>();
PseudoClasses.Set(pcDropdownOpen, newValue);
}
}
}

14
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -111,7 +111,7 @@ namespace Avalonia.Controls.Documents
private void AddText(string text)
{
if(Parent is RichTextBlock textBlock && !textBlock.HasComplexContent)
if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent)
{
textBlock._text += text;
}
@ -156,7 +156,17 @@ namespace Avalonia.Controls.Documents
{
foreach (var child in this)
{
((ISetLogicalParent)child).SetParent(parent);
var oldParent = child.Parent;
if (oldParent != parent)
{
if (oldParent != null)
{
((ISetLogicalParent)child).SetParent(null);
}
((ISetLogicalParent)child).SetParent(parent);
}
}
}

111
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -27,12 +27,22 @@ namespace Avalonia.Controls.Primitives
public static readonly AttachedProperty<bool> IsClipEnabledProperty =
AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, bool>("IsClipEnabled", true);
/// <summary>
/// Allows for getting and setting of the adorner for control.
/// </summary>
public static readonly AttachedProperty<Control?> AdornerProperty =
AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, Control?>("Adorner");
private static readonly AttachedProperty<AdornedElementInfo> s_adornedElementInfoProperty =
AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, AdornedElementInfo>("AdornedElementInfo");
private static readonly AttachedProperty<AdornerLayer?> s_savedAdornerLayerProperty =
AvaloniaProperty.RegisterAttached<Visual, Visual, AdornerLayer?>("SavedAdornerLayer");
static AdornerLayer()
{
AdornedElementProperty.Changed.Subscribe(AdornedElementChanged);
AdornerProperty.Changed.Subscribe(AdornerChanged);
}
public AdornerLayer()
@ -65,6 +75,107 @@ namespace Avalonia.Controls.Primitives
adorner.SetValue(IsClipEnabledProperty, isClipEnabled);
}
public static Control? GetAdorner(Visual visual)
{
return visual.GetValue(AdornerProperty);
}
public static void SetAdorner(Visual visual, Control? adorner)
{
visual.SetValue(AdornerProperty, adorner);
}
private static void AdornerChanged(AvaloniaPropertyChangedEventArgs<Control?> e)
{
if (e.Sender is Visual visual)
{
var oldAdorner = e.OldValue.GetValueOrDefault();
var newAdorner = e.NewValue.GetValueOrDefault();
if (Equals(oldAdorner, newAdorner))
{
return;
}
if (oldAdorner is { })
{
visual.AttachedToVisualTree -= VisualOnAttachedToVisualTree;
visual.DetachedFromVisualTree -= VisualOnDetachedFromVisualTree;
Detach(visual, oldAdorner);
}
if (newAdorner is { })
{
visual.AttachedToVisualTree += VisualOnAttachedToVisualTree;
visual.DetachedFromVisualTree += VisualOnDetachedFromVisualTree;
Attach(visual, newAdorner);
}
}
}
private static void VisualOnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (sender is Visual visual)
{
var adorner = GetAdorner(visual);
if (adorner is { })
{
Attach(visual, adorner);
}
}
}
private static void VisualOnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (sender is Visual visual)
{
var adorner = GetAdorner(visual);
if (adorner is { })
{
Detach(visual, adorner);
}
}
}
private static void Attach(Visual visual, Control adorner)
{
var layer = AdornerLayer.GetAdornerLayer(visual);
AddVisualAdorner(visual, adorner, layer);
visual.SetValue(s_savedAdornerLayerProperty, layer);
}
private static void Detach(Visual visual, Control adorner)
{
var layer = visual.GetValue(s_savedAdornerLayerProperty);
RemoveVisualAdorner(visual, adorner, layer);
visual.ClearValue(s_savedAdornerLayerProperty);
}
private static void AddVisualAdorner(Visual visual, Control? adorner, AdornerLayer? layer)
{
if (adorner is null || layer == null || layer.Children.Contains(adorner))
{
return;
}
AdornerLayer.SetAdornedElement(adorner, visual);
AdornerLayer.SetIsClipEnabled(adorner, false);
((ISetLogicalParent) adorner).SetParent(visual);
layer.Children.Add(adorner);
}
private static void RemoveVisualAdorner(Visual visual, Control? adorner, AdornerLayer? layer)
{
if (adorner is null || layer is null || !layer.Children.Contains(adorner))
{
return;
}
layer.Children.Remove(adorner);
((ISetLogicalParent) adorner).SetParent(null);
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (var child in Children)

34
src/Avalonia.Controls/Window.cs

@ -668,7 +668,7 @@ namespace Avalonia.Controls
Owner = parent;
parent?.AddChild(this, false);
SetWindowStartupLocation(Owner?.PlatformImpl);
SetWindowStartupLocation(parent?.PlatformImpl);
PlatformImpl?.Show(ShowActivated, false);
Renderer?.Start();
@ -830,10 +830,11 @@ namespace Avalonia.Controls
var startupLocation = WindowStartupLocation;
if (startupLocation == WindowStartupLocation.CenterOwner &&
Owner is Window ownerWindow &&
ownerWindow.WindowState == WindowState.Minimized)
(owner is null ||
(Owner is Window ownerWindow && ownerWindow.WindowState == WindowState.Minimized))
)
{
// If startup location is CenterOwner, but owner is minimized then fall back
// If startup location is CenterOwner, but owner is null or minimized then fall back
// to CenterScreen. This behavior is consistent with WPF.
startupLocation = WindowStartupLocation.CenterScreen;
}
@ -851,31 +852,24 @@ namespace Avalonia.Controls
if (owner is not null)
{
screen = Screens.ScreenFromWindow(owner);
screen ??= Screens.ScreenFromPoint(owner.Position);
screen = Screens.ScreenFromWindow(owner)
?? Screens.ScreenFromPoint(owner.Position);
}
if (screen is null)
{
screen = Screens.ScreenFromPoint(Position);
}
screen ??= Screens.ScreenFromPoint(Position);
if (screen != null)
if (screen is not null)
{
Position = screen.WorkingArea.CenterRect(rect).Position;
}
}
else if (startupLocation == WindowStartupLocation.CenterOwner)
{
if (owner != null)
{
var ownerSize = owner.FrameSize ?? owner.ClientSize;
var ownerRect = new PixelRect(
owner.Position,
PixelSize.FromSize(ownerSize, scaling));
Position = ownerRect.CenterRect(rect).Position;
}
var ownerSize = owner!.FrameSize ?? owner.ClientSize;
var ownerRect = new PixelRect(
owner.Position,
PixelSize.FromSize(ownerSize, scaling));
Position = ownerRect.CenterRect(rect).Position;
}
}

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

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

19
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@ -57,17 +57,8 @@
<Setter Property="Template">
<ControlTemplate>
<DataValidationErrors>
<Grid RowDefinitions="Auto, *, Auto" ColumnDefinitions="*,32">
<ContentPresenter x:Name="HeaderContentPresenter"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
IsVisible="False"
FontWeight="{DynamicResource ComboBoxHeaderThemeFontWeight}"
Margin="{DynamicResource ComboBoxTopHeaderMargin}"
VerticalAlignment="Top" />
<Grid ColumnDefinitions="*,32">
<Border x:Name="Background"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
@ -76,7 +67,6 @@
CornerRadius="{TemplateBinding CornerRadius}"
MinWidth="{DynamicResource ComboBoxThemeMinWidth}" />
<Border x:Name="HighlightBackground"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Background="{DynamicResource ComboBoxBackgroundUnfocused}"
@ -85,7 +75,6 @@
CornerRadius="{TemplateBinding CornerRadius}"
IsVisible="False"/>
<TextBlock x:Name="PlaceholderTextBlock"
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
@ -96,14 +85,12 @@
<ContentControl x:Name="ContentPresenter"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding ItemTemplate}"
Grid.Row="1"
Grid.Column="0"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
<Border x:Name="DropDownOverlay"
Grid.Row="1"
Grid.Column="1"
Background="Transparent"
Margin="0,1,1,1"
@ -112,7 +99,6 @@
HorizontalAlignment="Right" />
<PathIcon x:Name="DropDownGlyph"
Grid.Row="1"
Grid.Column="1"
UseLayoutRounding="False"
IsHitTestVisible="False"
@ -208,9 +194,6 @@
<Setter Property="Background" Value="{DynamicResource ComboBoxBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ComboBoxBorderBrushDisabled}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#HeaderContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundDisabled}" />
</Style>
<Style Selector="^ /template/ ContentControl#ContentPresenter">
<Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundDisabled}" />
</Style>

6
src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml

@ -153,11 +153,11 @@
</Style>
<!-- Changes foreground for watermark text when SelectedDate is null-->
<Style Selector="^:hasnodate /template/ Button#FlyoutButton TextBlock">
<Style Selector="^:hasnodate /template/ Button#PART_FlyoutButton TextBlock">
<Setter Property="Foreground" Value="{DynamicResource TextControlPlaceholderForeground}"/>
</Style>
<Style Selector="^:error /template/ Button#FlyoutButton">
<Style Selector="^:error /template/ Button#PART_FlyoutButton">
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlErrorTextForegroundBrush}"/>
</Style>
</ControlTheme>

4
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

@ -181,11 +181,11 @@
<Setter Property="Fill" Value="{DynamicResource TimePickerSpacerFillDisabled}"/>
</Style>
<Style Selector="^:hasnotime /template/ Button#FlyoutButton TextBlock">
<Style Selector="^:hasnotime /template/ Button#PART_FlyoutButton TextBlock">
<Setter Property="Foreground" Value="{DynamicResource TextControlPlaceholderForeground}"/>
</Style>
<Style Selector="^:error /template/ Button#FlyoutButton">
<Style Selector="^:error /template/ Button#PART_FlyoutButton">
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlErrorTextForegroundBrush}"/>
</Style>
</ControlTheme>

2
src/Avalonia.Themes.Simple/Controls/DatePicker.xaml

@ -170,7 +170,7 @@
</Style>
<!-- Changes foreground for watermark text when SelectedDate is null -->
<Style Selector="^:hasnodate /template/ Button#FlyoutButton TextBlock">
<Style Selector="^:hasnodate /template/ Button#PART_FlyoutButton TextBlock">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundLowBrush}" />
</Style>
</ControlTheme>

2
src/Avalonia.Themes.Simple/Controls/TimePicker.xaml

@ -189,7 +189,7 @@
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
</Style>
<Style Selector="^:hasnotime /template/ Button#FlyoutButton TextBlock">
<Style Selector="^:hasnotime /template/ Button#PART_FlyoutButton TextBlock">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundLowBrush}" />
</Style>
</ControlTheme>

2
src/Avalonia.X11/X11Window.cs

@ -302,7 +302,7 @@ namespace Avalonia.X11
min_height = min.Height
};
hints.height_inc = hints.width_inc = 1;
var flags = XSizeHintsFlags.PMinSize | XSizeHintsFlags.PResizeInc;
var flags = XSizeHintsFlags.PMinSize | XSizeHintsFlags.PResizeInc | XSizeHintsFlags.PPosition | XSizeHintsFlags.PSize;
// People might be passing double.MaxValue
if (max.Width < 100000 && max.Height < 100000)
{

2
src/Shared/ModuleInitializer.cs

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

1
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

@ -287,7 +287,6 @@ namespace Avalonia.Web.Blazor
// create the SkiaSharp context
if (_context == null)
{
Console.WriteLine("create glcontext");
_glInterface = GRGlInterface.Create();
_context = GRContext.CreateGl(_glInterface);

41
src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<WasmMainJSPath>main.js</WasmMainJSPath>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<WasmBuildNative>true</WasmBuildNative>
<EmccFlags>-sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0</EmccFlags>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<RunAOTCompilation>true</RunAOTCompilation>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>full</TrimMode>
<WasmBuildNative>true</WasmBuildNative>
<InvariantGlobalization>true</InvariantGlobalization>
<WasmEnableSIMD>true</WasmEnableSIMD>
<EmccCompileOptimizationFlag>-O3</EmccCompileOptimizationFlag>
<EmccLinkOptimizationFlag>-O3</EmccLinkOptimizationFlag>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\samples\ControlCatalog\ControlCatalog.csproj" />
<ProjectReference Include="..\Avalonia.Web\Avalonia.Web.csproj" />
<ProjectReference Include="..\..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<ItemGroup>
<WasmExtraFilesToDeploy Include="index.html" />
<WasmExtraFilesToDeploy Include="main.js" />
<WasmExtraFilesToDeploy Include="embed.js" />
<WasmExtraFilesToDeploy Include="favicon.ico" />
<WasmExtraFilesToDeploy Include="Logo.svg" />
<WasmExtraFilesToDeploy Include="app.css" />
</ItemGroup>
<Import Project="..\Avalonia.Web\Avalonia.Web.props" />
<Import Project="..\Avalonia.Web\Avalonia.Web.targets" />
</Project>

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

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

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Web;
using ControlCatalog;
using ControlCatalog.Web;
internal partial class Program
{
private static void Main(string[] args)
{
BuildAvaloniaApp()
.AfterSetup(_ =>
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
}).SetupBrowserApp("out");
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>();
}

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

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

@ -0,0 +1,31 @@
<!DOCTYPE html>
<!-- Licensed to the .NET Foundation under one or more agreements. -->
<!-- The .NET Foundation licenses this file to you under the MIT license. -->
<html>
<head>
<title>Avalonia.Web.Sample</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="modulepreload" href="./main.js" />
<link rel="modulepreload" href="./dotnet.js" />
<link rel="modulepreload" href="./avalonia.js" />
<link rel="stylesheet" href="./app.css" />
</head>
<body style="margin: 0px">
<div id="out">
<div id="avalonia-splash">
<div class="center">
<h2>Powered by</h2>
<a class="navbar-brand" href="https://www.avaloniaui.net/" target="_blank">
<img src="Logo.svg" alt="Avalonia Logo" width="30" height="24" />
Avalonia
</a>
</div>
</div>
</div>
<script type='module' src="./main.js"></script>
</body>
</html>

19
src/Web/Avalonia.Web.Sample/main.js

@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { dotnet } from './dotnet.js'
import { createAvaloniaRuntime } from './avalonia.js';
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
const dotnetRuntime = await dotnet
.withDiagnosticTracing(false)
.withApplicationArgumentsFromQuery()
.create();
await createAvaloniaRuntime(dotnetRuntime);
const config = dotnetRuntime.getConfig();
await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]);

11
src/Web/Avalonia.Web.Sample/runtimeconfig.template.json

@ -0,0 +1,11 @@
{
"wasmHostProperties": {
"perHostConfig": [
{
"name": "browser",
"html-path": "index.html",
"Host": "browser"
}
]
}
}

55
src/Web/Avalonia.Web/Avalonia.Web.csproj

@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>
<Import Project="..\..\..\build\BuildTargets.targets" />
<Import Project="..\..\..\build\SkiaSharp.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="*.props">
<Pack>true</Pack>
<PackagePath>build\</PackagePath>
</Content>
<Content Include="*.targets">
<Pack>true</Pack>
<PackagePath>build\;buildTransitive\</PackagePath>
</Content>
<Content Include="interop.js">
<Pack>true</Pack>
<PackagePath>build/interop.js;buildTransitive/interop.js</PackagePath>
</Content>
<Content Include="wwwroot/**/*.*">
<Pack>true</Pack>
<PackagePath>build\wwwroot;buildTransitive\wwwroot</PackagePath>
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<Target Name="NpmInstall" Inputs="webapp/package.json" Outputs="webapp/node_modules/.install-stamp">
<Exec Command="npm install" WorkingDirectory="webapp" />
<!-- Write the stamp file, so incremental builds work -->
<Touch Files="webapp/node_modules/.install-stamp" AlwaysCreate="true" />
</Target>
<Target Name="NpmRunBuild" DependsOnTargets="NpmInstall" BeforeTargets="BeforeBuild">
<Exec Command="npm run build" WorkingDirectory="webapp" />
</Target>
</Project>

5
src/Web/Avalonia.Web/Avalonia.Web.props

@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<EmccExtraLDFlags>$(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js"</EmccExtraLDFlags>
</PropertyGroup>
</Project>

7
src/Web/Avalonia.Web/Avalonia.Web.targets

@ -0,0 +1,7 @@
<Project>
<ItemGroup>
<WasmExtraFilesToDeploy Include="$(MSBuildThisFileDirectory)/wwwroot/**/*.*" />
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\3.1.7\libHarfBuzzSharp.a" />
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\3.1.7\libSkiaSharp.a" />
</ItemGroup>
</Project>

451
src/Web/Avalonia.Web/AvaloniaView.cs

@ -0,0 +1,451 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Web.Interop;
using Avalonia.Web.Skia;
using SkiaSharp;
namespace Avalonia.Web
{
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
public partial class AvaloniaView : ITextInputMethodImpl
{
private readonly BrowserTopLevelImpl _topLevelImpl;
private EmbeddableControlRoot _topLevel;
private readonly JSObject _containerElement;
private readonly JSObject _canvas;
private readonly JSObject _nativeControlsContainer;
private readonly JSObject _inputElement;
private readonly JSObject? _splash;
private GLInfo? _jsGlInfo = null;
private double _dpi = 1;
private Size _canvasSize = new(100.0, 100.0);
private GRContext? _context;
private GRGlInterface? _glInterface;
private const SKColorType ColorType = SKColorType.Rgba8888;
private bool _useGL;
private ITextInputMethodClient? _client;
private static int _canvasCount;
public AvaloniaView(string divId)
{
var host = DomHelper.GetElementById(divId);
if (host == null)
{
throw new Exception($"Element with id {divId} was not found in the html document.");
}
var hostContent = DomHelper.CreateAvaloniaHost(host);
if (hostContent == null)
{
throw new InvalidOperationException("Avalonia WASM host wasn't initialized.");
}
_containerElement = hostContent.GetPropertyAsJSObject("host")
?? throw new InvalidOperationException("Host cannot be null");
_canvas = hostContent.GetPropertyAsJSObject("canvas")
?? throw new InvalidOperationException("Canvas cannot be null");
_nativeControlsContainer = hostContent.GetPropertyAsJSObject("nativeHost")
?? throw new InvalidOperationException("NativeHost cannot be null");
_inputElement = hostContent.GetPropertyAsJSObject("inputElement")
?? throw new InvalidOperationException("InputElement cannot be null");
_splash = DomHelper.GetElementById("avalonia-splash");
_canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}");
_topLevelImpl = new BrowserTopLevelImpl(this);
_topLevel = new WebEmbeddableControlRoot(_topLevelImpl, () =>
{
Dispatcher.UIThread.Post(() =>
{
if (_splash != null)
{
DomHelper.AddCssClass(_splash, "splash-close");
}
});
});
_topLevelImpl.SetCssCursor = (cursor) =>
{
InputHelper.SetCursor(_containerElement, cursor); // macOS
InputHelper.SetCursor(_canvas, cursor); // windows
};
_topLevel.Prepare();
_topLevel.Renderer.Start();
InputHelper.SubscribeKeyEvents(
_containerElement,
OnKeyDown,
OnKeyUp);
InputHelper.SubscribeTextEvents(
_inputElement,
OnTextInput,
OnCompositionStart,
OnCompositionUpdate,
OnCompositionEnd);
InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel);
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>();
_dpi = DomHelper.ObserveDpi(OnDpiChanged);
_useGL = skiaOptions?.CustomGpuFactory != null;
if (_useGL)
{
_jsGlInfo = CanvasHelper.InitialiseGL(_canvas, OnRenderFrame);
// create the SkiaSharp context
if (_context == null)
{
_glInterface = GRGlInterface.Create();
_context = GRContext.CreateGl(_glInterface);
// bump the default resource cache limit
_context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024);
}
_topLevelImpl.Surfaces = new[] { new BrowserSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) };
}
else
{
//var rasterInitialized = _interop.InitRaster();
//Console.WriteLine("raster initialized: {0}", rasterInitialized);
//_topLevelImpl.SetSurface(ColorType,
// new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData);
}
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
DomHelper.ObserveSize(host, divId, OnSizeChanged);
CanvasHelper.RequestAnimationFrame(_canvas, true);
}
private static RawPointerPoint ExtractRawPointerFromJSArgs(JSObject args)
{
var point = new RawPointerPoint
{
Position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")),
Pressure = (float)args.GetPropertyAsDouble("pressure"),
XTilt = (float)args.GetPropertyAsDouble("tiltX"),
YTilt = (float)args.GetPropertyAsDouble("tiltY"),
Twist = (float)args.GetPropertyAsDouble("twist")
};
return point;
}
private bool OnPointerMove(JSObject args)
{
var type = args.GetPropertyAsString("pointertype");
var point = ExtractRawPointerFromJSArgs(args);
return _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
}
private bool OnPointerDown(JSObject args)
{
var pointerType = args.GetPropertyAsString("pointerType");
var type = pointerType switch
{
"touch" => RawPointerEventType.TouchBegin,
_ => args.GetPropertyAsInt32("button") switch
{
0 => RawPointerEventType.LeftButtonDown,
1 => RawPointerEventType.MiddleButtonDown,
2 => RawPointerEventType.RightButtonDown,
3 => RawPointerEventType.XButton1Down,
4 => RawPointerEventType.XButton2Down,
// 5 => Pen eraser button,
_ => RawPointerEventType.Move
}
};
var point = ExtractRawPointerFromJSArgs(args);
return _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
}
private bool OnPointerUp(JSObject args)
{
var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse";
var type = pointerType switch
{
"touch" => RawPointerEventType.TouchEnd,
_ => args.GetPropertyAsInt32("button") switch
{
0 => RawPointerEventType.LeftButtonUp,
1 => RawPointerEventType.MiddleButtonUp,
2 => RawPointerEventType.RightButtonUp,
3 => RawPointerEventType.XButton1Up,
4 => RawPointerEventType.XButton2Up,
// 5 => Pen eraser button,
_ => RawPointerEventType.Move
}
};
var point = ExtractRawPointerFromJSArgs(args);
return _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId"));
}
private bool OnWheel(JSObject args)
{
return _topLevelImpl.RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")),
new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), GetModifiers(args));
}
private static RawInputModifiers GetModifiers(JSObject e)
{
var modifiers = RawInputModifiers.None;
if (e.GetPropertyAsBoolean("ctrlKey"))
modifiers |= RawInputModifiers.Control;
if (e.GetPropertyAsBoolean("altKey"))
modifiers |= RawInputModifiers.Alt;
if (e.GetPropertyAsBoolean("shiftKey"))
modifiers |= RawInputModifiers.Shift;
if (e.GetPropertyAsBoolean("metaKey"))
modifiers |= RawInputModifiers.Meta;
var buttons = e.GetPropertyAsInt32("buttons");
if ((buttons & 1L) == 1)
modifiers |= RawInputModifiers.LeftMouseButton;
if ((buttons & 2L) == 2)
modifiers |= e.GetPropertyAsString("type") == "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton;
if ((buttons & 4L) == 4)
modifiers |= RawInputModifiers.MiddleMouseButton;
if ((buttons & 8L) == 8)
modifiers |= RawInputModifiers.XButton1MouseButton;
if ((buttons & 16L) == 16)
modifiers |= RawInputModifiers.XButton2MouseButton;
if ((buttons & 32L) == 32)
modifiers |= RawInputModifiers.PenEraser;
return modifiers;
}
private bool OnKeyDown (string code, string key, int modifier)
{
return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier);
}
private bool OnKeyUp(string code, string key, int modifier)
{
return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier);
}
private bool OnTextInput (string type, string? data)
{
if(data == null || IsComposing)
{
return false;
}
return _topLevelImpl.RawTextEvent(data);
}
private bool OnCompositionStart (JSObject args)
{
if (_client == null)
return false;
_client.SetPreeditText(null);
IsComposing = true;
return false;
}
private bool OnCompositionUpdate(JSObject args)
{
if (_client == null)
return false;
_client.SetPreeditText(args.GetPropertyAsString("data"));
return false;
}
private bool OnCompositionEnd(JSObject args)
{
if (_client == null)
return false;
IsComposing = false;
_client.SetPreeditText(null);
_topLevelImpl.RawTextEvent(args.GetPropertyAsString("data")!);
return false;
}
private void OnRenderFrame()
{
if (_useGL && (_jsGlInfo == null))
{
Console.WriteLine("nothing to render");
return;
}
if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0)
{
Console.WriteLine("nothing to render");
return;
}
ManualTriggerRenderTimer.Instance.RaiseTick();
}
public Control? Content
{
get => (Control)_topLevel.Content!;
set => _topLevel.Content = value;
}
public bool IsComposing { get; private set; }
internal INativeControlHostImpl GetNativeControlHostImpl()
{
return new BrowserNativeControlHost(_nativeControlsContainer);
}
private void ForceBlit()
{
// Note: this is technically a hack, but it's a kinda unique use case when
// we want to blit the previous frame
// renderer doesn't have much control over the render target
// we render on the UI thread
// We also don't want to have it as a meaningful public API.
// Therefore we have InternalsVisibleTo hack here.
if (_topLevel.Renderer is CompositingRenderer dr)
{
dr.CompositionTarget.ImmediateUIThreadRender();
}
}
private void OnDpiChanged(double oldDpi, double newDpi)
{
if (Math.Abs(_dpi - newDpi) > 0.0001)
{
_dpi = newDpi;
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
ForceBlit();
}
}
private void OnSizeChanged(int height, int width)
{
var newSize = new Size(height, width);
if (_canvasSize != newSize)
{
_canvasSize = newSize;
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
ForceBlit();
}
}
private void HideIme()
{
InputHelper.HideElement(_inputElement);
InputHelper.FocusElement(_containerElement);
}
public void SetClient(ITextInputMethodClient? client)
{
Console.WriteLine("Set Client");
if (_client != null)
{
_client.SurroundingTextChanged -= SurroundingTextChanged;
}
if (client != null)
{
client.SurroundingTextChanged += SurroundingTextChanged;
}
InputHelper.ClearInputElement(_inputElement);
_client = client;
if (_client != null)
{
InputHelper.ShowElement(_inputElement);
InputHelper.FocusElement(_inputElement);
var surroundingText = _client.SurroundingText;
InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
Console.WriteLine("Shown, focused and surrounded.");
}
else
{
HideIme();
}
}
private void SurroundingTextChanged(object? sender, EventArgs e)
{
if (_client != null)
{
var surroundingText = _client.SurroundingText;
InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
}
public void SetCursorRect(Rect rect)
{
InputHelper.FocusElement(_inputElement);
InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.SurroundingText.CursorOffset ?? 0);
InputHelper.FocusElement(_inputElement);
}
public void SetOptions(TextInputOptions options)
{
}
public void Reset()
{
InputHelper.ClearInputElement(_inputElement);
InputHelper.SetSurroundingText(_inputElement, "", 0, 0);
}
}
}

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

41
src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs

@ -0,0 +1,41 @@
using System.Runtime.InteropServices.JavaScript;
using System;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
using Avalonia.Web.Skia;
namespace Avalonia.Web
{
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime
{
public AvaloniaView? View;
public Control? MainView
{
get => View!.Content;
set => View!.Content = value;
}
}
public static partial class WebAppBuilder
{
public static T SetupBrowserApp<T>(
this T builder, string mainDivId)
where T : AppBuilderBase<T>, new()
{
var lifetime = new BrowserSingleViewLifetime();
return builder
.UseWindowingSubsystem(BrowserWindowingPlatform.Register)
.UseSkia()
.With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() })
.AfterSetup(b =>
{
lifetime.View = new AvaloniaView(mainDivId);
})
.SetupWithLifetime(lifetime);
}
}
}

226
src/Web/Avalonia.Web/BrowserTopLevelImpl.cs

@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Web.Skia;
using Avalonia.Web.Storage;
namespace Avalonia.Web
{
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
{
private Size _clientSize;
private IInputRoot? _inputRoot;
private readonly Stopwatch _sw = Stopwatch.StartNew();
private readonly AvaloniaView _avaloniaView;
private readonly TouchDevice _touchDevice;
private readonly PenDevice _penDevice;
private string _currentCursor = CssCursor.Default;
public BrowserTopLevelImpl(AvaloniaView avaloniaView)
{
Surfaces = Enumerable.Empty<object>();
_avaloniaView = avaloniaView;
TransparencyLevel = WindowTransparencyLevel.None;
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
_touchDevice = new TouchDevice();
_penDevice = new PenDevice();
NativeControlHost = _avaloniaView.GetNativeControlHostImpl();
}
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
public void SetClientSize(Size newSize, double dpi)
{
if (Math.Abs(RenderScaling - dpi) > 0.0001)
{
if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface)
{
surface.Scaling = dpi;
}
ScalingChanged?.Invoke(dpi);
}
if (newSize != _clientSize)
{
_clientSize = newSize;
if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface)
{
surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height);
}
Resized?.Invoke(newSize, PlatformResizeReason.User);
}
}
public bool RawPointerEvent(
RawPointerEventType eventType, string pointerType,
RawPointerPoint p, RawInputModifiers modifiers, long touchPointId)
{
if (_inputRoot is { }
&& Input is { } input)
{
var device = GetPointerDevice(pointerType);
var args = device is TouchDevice ?
new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId) :
new RawPointerEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers)
{
RawPointerId = touchPointId
};
input.Invoke(args);
return args.Handled;
}
return false;
}
private IPointerDevice GetPointerDevice(string pointerType)
{
return pointerType switch
{
"touch" => _touchDevice,
"pen" => _penDevice,
_ => MouseDevice
};
}
public bool RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers)
{
if (_inputRoot is { })
{
var args = new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers);
Input?.Invoke(args);
return args.Handled;
}
return false;
}
public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers)
{
if (Keycodes.KeyCodes.TryGetValue(code, out var avkey))
{
if (_inputRoot is { })
{
var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers);
Input?.Invoke(args);
return args.Handled;
}
}
else if (Keycodes.KeyCodes.TryGetValue(key, out avkey))
{
if (_inputRoot is { })
{
var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers);
Input?.Invoke(args);
return args.Handled;
}
}
return false;
}
public bool RawTextEvent(string text)
{
if (_inputRoot is { })
{
var args = new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text);
Input?.Invoke(args);
return args.Handled;
}
return false;
}
public void Dispose()
{
}
public IRenderer CreateRenderer(IRenderRoot root)
{
var loop = AvaloniaLocator.Current.GetRequiredService<IRenderLoop>();
return new CompositingRenderer(root, new Compositor(loop, null));
}
public void Invalidate(Rect rect)
{
//Console.WriteLine("invalidate rect called");
}
public void SetInputRoot(IInputRoot inputRoot)
{
_inputRoot = inputRoot;
}
public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y);
public PixelPoint PointToScreen(Point point) => new PixelPoint((int)point.X, (int)point.Y);
public void SetCursor(ICursorImpl? cursor)
{
var val = (cursor as CssCursor)?.Value ?? CssCursor.Default;
if (_currentCursor != val)
{
SetCssCursor?.Invoke(val);
_currentCursor = val;
}
}
public IPopupImpl? CreatePopup()
{
return null;
}
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{
}
public Size ClientSize => _clientSize;
public Size? FrameSize => null;
public double RenderScaling => (Surfaces.FirstOrDefault() as BrowserSkiaSurface)?.Scaling ?? 1;
public IEnumerable<object> Surfaces { get; set; }
public Action<string>? SetCssCursor { get; set; }
public Action<RawInputEventArgs>? Input { get; set; }
public Action<Rect>? Paint { get; set; }
public Action<Size, PlatformResizeReason>? Resized { get; set; }
public Action<double>? ScalingChanged { get; set; }
public Action<WindowTransparencyLevel>? TransparencyLevelChanged { get; set; }
public Action? Closed { get; set; }
public Action? LostFocus { get; set; }
public IMouseDevice MouseDevice { get; } = new MouseDevice();
public IKeyboardDevice KeyboardDevice { get; } = BrowserWindowingPlatform.Keyboard;
public WindowTransparencyLevel TransparencyLevel { get; }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; }
public ITextInputMethodImpl TextInputMethod => _avaloniaView;
public INativeControlHostImpl? NativeControlHost { get; }
public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider();
}
}

29
src/Web/Avalonia.Web/ClipboardImpl.cs

@ -0,0 +1,29 @@
using System;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Web.Interop;
namespace Avalonia.Web
{
internal class ClipboardImpl : IClipboard
{
public Task<string> GetTextAsync()
{
return InputHelper.ReadClipboardTextAsync();
}
public Task SetTextAsync(string text)
{
return InputHelper.WriteClipboardTextAsync(text);
}
public async Task ClearAsync() => await SetTextAsync("");
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object> GetDataAsync(string format) => Task.FromResult<object>(new());
}
}

95
src/Web/Avalonia.Web/Cursor.cs

@ -0,0 +1,95 @@
using System;
using System.IO;
using Avalonia.Input;
using Avalonia.Platform;
namespace Avalonia.Web
{
public class CssCursor : ICursorImpl
{
public const string Default = "default";
public string? Value { get; set; }
public CssCursor(StandardCursorType type)
{
Value = ToKeyword(type);
}
/// <summary>
/// Create a cursor from base64 image
/// </summary>
public CssCursor(string base64, string format, PixelPoint hotspot, StandardCursorType fallback)
{
Value = $"url(\"data:image/{format};base64,{base64}\") {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}";
}
/// <summary>
/// Create a cursor from url to *.cur file.
/// </summary>
public CssCursor(string url, StandardCursorType fallback)
{
Value = $"url('{url}'), {ToKeyword(fallback)}";
}
/// <summary>
/// Create a cursor from png/svg and hotspot position
/// </summary>
public CssCursor(string url, PixelPoint hotSpot, StandardCursorType fallback)
{
Value = $"url('{url}') {hotSpot.X} {hotSpot.Y}, {ToKeyword(fallback)}";
}
private static string ToKeyword(StandardCursorType type) => type switch
{
StandardCursorType.Hand => "pointer",
StandardCursorType.Cross => "crosshair",
StandardCursorType.Help => "help",
StandardCursorType.Ibeam => "text",
StandardCursorType.No => "not-allowed",
StandardCursorType.None => "none",
StandardCursorType.Wait => "progress",
StandardCursorType.AppStarting => "wait",
StandardCursorType.DragMove => "move",
StandardCursorType.DragCopy => "copy",
StandardCursorType.DragLink => "alias",
StandardCursorType.UpArrow => "default",/*not found matching one*/
StandardCursorType.SizeWestEast => "ew-resize",
StandardCursorType.SizeNorthSouth => "ns-resize",
StandardCursorType.SizeAll => "move",
StandardCursorType.TopSide => "n-resize",
StandardCursorType.BottomSide => "s-resize",
StandardCursorType.LeftSide => "w-resize",
StandardCursorType.RightSide => "e-resize",
StandardCursorType.TopLeftCorner => "nw-resize",
StandardCursorType.TopRightCorner => "ne-resize",
StandardCursorType.BottomLeftCorner => "sw-resize",
StandardCursorType.BottomRightCorner => "se-resize",
_ => Default,
};
public void Dispose() {}
}
internal class CssCursorFactory : ICursorFactory
{
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot)
{
using var imageStream = new MemoryStream();
cursor.Save(imageStream);
//not memory optimized because CryptoStream with ToBase64Transform is not supported in the browser.
var base64String = Convert.ToBase64String(imageStream.ToArray());
return new CssCursor(base64String, "png", hotSpot, StandardCursorType.Arrow);
}
ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType)
{
return new CssCursor(cursorType);
}
}
}

43
src/Web/Avalonia.Web/Interop/CanvasHelper.cs

@ -0,0 +1,43 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
namespace Avalonia.Web.Interop;
internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth);
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
internal static partial class CanvasHelper
{
[DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)]
static extern JSObject InterceptGLObject();
public static GLInfo InitialiseGL(JSObject canvas, Action renderFrameCallback)
{
InterceptGLObject();
var info = InitGL(canvas, canvas.GetPropertyAsString("id")!, renderFrameCallback);
var glInfo = new GLInfo(
info.GetPropertyAsInt32("context"),
(uint)info.GetPropertyAsInt32("fboId"),
info.GetPropertyAsInt32("stencil"),
info.GetPropertyAsInt32("sample"),
info.GetPropertyAsInt32("depth"));
return glInfo;
}
[JSImport("Canvas.requestAnimationFrame", "avalonia.ts")]
public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop);
[JSImport("Canvas.setCanvasSize", "avalonia.ts")]
public static partial void SetCanvasSize(JSObject canvas, int height, int width);
[JSImport("Canvas.initGL", "avalonia.ts")]
private static partial JSObject InitGL(
JSObject canvas,
string canvasId,
[JSMarshalAs<JSType.Function>] Action renderFrameCallback);
}

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

@ -0,0 +1,28 @@
using System;
using System.Runtime.InteropServices.JavaScript;
namespace Avalonia.Web.Interop;
internal static partial class DomHelper
{
[JSImport("globalThis.document.getElementById")]
internal static partial JSObject? GetElementById(string id);
[JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")]
public static partial JSObject CreateAvaloniaHost(JSObject element);
[JSImport("AvaloniaDOM.addClass", "avalonia.ts")]
public static partial void AddCssClass(JSObject element, string className);
[JSImport("SizeWatcher.observe", "avalonia.ts")]
public static partial JSObject ObserveSize(
JSObject canvas,
string canvasId,
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>]
Action<int, int> onSizeChanged);
[JSImport("DpiWatcher.start", "avalonia.ts")]
public static partial double ObserveDpi(
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>]
Action<double, double> onDpiChanged);
}

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

@ -0,0 +1,78 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Avalonia.Web.Interop;
internal static partial class InputHelper
{
[JSImport("InputHelper.subscribeKeyEvents", "avalonia.ts")]
public static partial void SubscribeKeyEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>]
Func<string, string, int, bool> keyDown,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>]
Func<string, string, int, bool> keyUp);
[JSImport("InputHelper.subscribeTextEvents", "avalonia.ts")]
public static partial void SubscribeTextEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Boolean>>]
Func<string, string?, bool> onInput,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> onCompositionStart,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> onCompositionUpdate,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> onCompositionEnd);
[JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")]
public static partial void SubscribePointerEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> pointerMove,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> pointerDown,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> pointerUp,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> wheel);
[JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")]
public static partial void SubscribeInputEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.Boolean>>]
Func<string, bool> input);
[JSImport("InputHelper.clearInput", "avalonia.ts")]
public static partial void ClearInputElement(JSObject htmlElement);
[JSImport("InputHelper.isInputElement", "avalonia.ts")]
public static partial void IsInputElement(JSObject htmlElement);
[JSImport("InputHelper.focusElement", "avalonia.ts")]
public static partial void FocusElement(JSObject htmlElement);
[JSImport("InputHelper.setCursor", "avalonia.ts")]
public static partial void SetCursor(JSObject htmlElement, string kind);
[JSImport("InputHelper.hide", "avalonia.ts")]
public static partial void HideElement(JSObject htmlElement);
[JSImport("InputHelper.show", "avalonia.ts")]
public static partial void ShowElement(JSObject htmlElement);
[JSImport("InputHelper.setSurroundingText", "avalonia.ts")]
public static partial void SetSurroundingText(JSObject htmlElement, string text, int start, int end);
[JSImport("InputHelper.setBounds", "avalonia.ts")]
public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret);
[JSImport("globalThis.navigator.clipboard.readText")]
public static partial Task<string> ReadClipboardTextAsync();
[JSImport("globalThis.navigator.clipboard.writeText")]
public static partial Task WriteClipboardTextAsync(string text);
}

28
src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs

@ -0,0 +1,28 @@
using System;
using System.Runtime.InteropServices.JavaScript;
namespace Avalonia.Web.Interop;
internal static partial class NativeControlHostHelper
{
[JSImport("NativeControlHost.createDefaultChild", "avalonia.ts")]
internal static partial JSObject CreateDefaultChild(JSObject? parent);
[JSImport("NativeControlHost.createAttachment", "avalonia.ts")]
internal static partial JSObject CreateAttachment();
[JSImport("NativeControlHost.initializeWithChildHandle", "avalonia.ts")]
internal static partial void InitializeWithChildHandle(JSObject element, JSObject child);
[JSImport("NativeControlHost.attachTo", "avalonia.ts")]
internal static partial void AttachTo(JSObject element, JSObject? host);
[JSImport("NativeControlHost.showInBounds", "avalonia.ts")]
internal static partial void ShowInBounds(JSObject element, double x, double y, double width, double height);
[JSImport("NativeControlHost.hideWithSize", "avalonia.ts")]
internal static partial void HideWithSize(JSObject element, double width, double height);
[JSImport("NativeControlHost.releaseChild", "avalonia.ts")]
internal static partial void ReleaseChild(JSObject element);
}

55
src/Web/Avalonia.Web/Interop/StorageHelper.cs

@ -0,0 +1,55 @@
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Avalonia.Web.Interop;
internal static partial class StorageHelper
{
[JSImport("Caniuse.canShowOpenFilePicker", "avalonia.ts")]
public static partial bool CanShowOpenFilePicker();
[JSImport("Caniuse.canShowSaveFilePicker", "avalonia.ts")]
public static partial bool CanShowSaveFilePicker();
[JSImport("Caniuse.canShowDirectoryPicker", "avalonia.ts")]
public static partial bool CanShowDirectoryPicker();
[JSImport("StorageProvider.selectFolderDialog", "storage.ts")]
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn);
[JSImport("StorageProvider.openFileDialog", "storage.ts")]
public static partial Task<JSObject?> OpenFileDialog(JSObject? startIn, bool multiple,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSImport("StorageProvider.saveFileDialog", "storage.ts")]
public static partial Task<JSObject?> SaveFileDialog(JSObject? startIn, string? suggestedName,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSImport("StorageProvider.openBookmark", "storage.ts")]
public static partial Task<JSObject?> OpenBookmark(string key);
[JSImport("StorageItem.saveBookmark", "storage.ts")]
public static partial Task<string?> SaveBookmark(JSObject item);
[JSImport("StorageItem.deleteBookmark", "storage.ts")]
public static partial Task DeleteBookmark(JSObject item);
[JSImport("StorageItem.getProperties", "storage.ts")]
public static partial Task<JSObject?> GetProperties(JSObject item);
[JSImport("StorageItem.openWrite", "storage.ts")]
public static partial Task<JSObject> OpenWrite(JSObject item);
[JSImport("StorageItem.openRead", "storage.ts")]
public static partial Task<JSObject> OpenRead(JSObject item);
[JSImport("StorageItem.getItems", "storage.ts")]
[return: JSMarshalAs<JSType.Promise<JSType.Object>>]
public static partial Task<JSObject> GetItems(JSObject item);
[JSImport("StorageItems.itemsArray", "storage.ts")]
public static partial JSObject[] ItemsArray(JSObject item);
[JSImport("StorageProvider.createAcceptType", "storage.ts")]
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes);
}

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

@ -0,0 +1,40 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Avalonia.Web.Storage;
/// <summary>
/// Set of FileSystemWritableFileStream and Blob methods.
/// </summary>
internal static partial class StreamHelper
{
[JSImport("StreamHelper.seek", "avalonia.ts")]
public static partial void Seek(JSObject stream, [JSMarshalAs<JSType.Number>] long position);
[JSImport("StreamHelper.truncate", "avalonia.ts")]
public static partial void Truncate(JSObject stream, [JSMarshalAs<JSType.Number>] long size);
[JSImport("StreamHelper.write", "avalonia.ts")]
public static partial Task WriteAsync(JSObject stream, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> data);
[JSImport("StreamHelper.close", "avalonia.ts")]
public static partial Task CloseAsync(JSObject stream);
[JSImport("StreamHelper.byteLength", "avalonia.ts")]
[return: JSMarshalAs<JSType.Number>]
public static partial long ByteLength(JSObject stream);
[JSImport("StreamHelper.sliceArrayBuffer", "avalonia.ts")]
private static partial Task<JSObject> SliceToArrayBuffer(JSObject stream, [JSMarshalAs<JSType.Number>] long offset, int count);
[JSImport("StreamHelper.toMemoryView", "avalonia.ts")]
[return: JSMarshalAs<JSType.Array<JSType.Number>>]
private static partial byte[] ArrayBufferToMemoryView(JSObject stream);
public static async Task<byte[]> SliceAsync(JSObject stream, long offset, int count)
{
using var buffer = await SliceToArrayBuffer(stream, offset, count);
return ArrayBufferToMemoryView(buffer);
}
}

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

129
src/Web/Avalonia.Web/Keycodes.cs

@ -0,0 +1,129 @@
using System.Collections.Generic;
using Avalonia.Input;
namespace Avalonia.Web
{
internal static class Keycodes
{
public static Dictionary<string, Key> KeyCodes = new()
{
{ "Escape", Key.Escape },
{ "Digit1", Key.D1 },
{ "Digit2", Key.D2 },
{ "Digit3", Key.D3 },
{ "Digit4", Key.D4 },
{ "Digit5", Key.D5 },
{ "Digit6", Key.D6 },
{ "Digit7", Key.D7 },
{ "Digit8", Key.D8 },
{ "Digit9", Key.D9 },
{ "Digit0", Key.D0 },
{ "Minus", Key.OemMinus },
//{ "Equal" , Key. },
{ "Backspace", Key.Back },
{ "Tab", Key.Tab },
{ "KeyQ", Key.Q },
{ "KeyW", Key.W },
{ "KeyE", Key.E },
{ "KeyR", Key.R },
{ "KeyT", Key.T },
{ "KeyY", Key.Y },
{ "KeyU", Key.U },
{ "KeyI", Key.I },
{ "KeyO", Key.O },
{ "KeyP", Key.P },
{ "BracketLeft", Key.OemOpenBrackets },
{ "BracketRight", Key.OemCloseBrackets },
{ "Enter", Key.Enter },
{ "ControlLeft", Key.LeftCtrl },
{ "KeyA", Key.A },
{ "KeyS", Key.S },
{ "KeyD", Key.D },
{ "KeyF", Key.F },
{ "KeyG", Key.G },
{ "KeyH", Key.H },
{ "KeyJ", Key.J },
{ "KeyK", Key.K },
{ "KeyL", Key.L },
{ "Semicolon", Key.OemSemicolon },
{ "Quote", Key.OemQuotes },
//{ "Backquote" , Key. },
{ "ShiftLeft", Key.LeftShift },
{ "Backslash", Key.OemBackslash },
{ "KeyZ", Key.Z },
{ "KeyX", Key.X },
{ "KeyC", Key.C },
{ "KeyV", Key.V },
{ "KeyB", Key.B },
{ "KeyN", Key.N },
{ "KeyM", Key.M },
{ "Comma", Key.OemComma },
{ "Period", Key.OemPeriod },
//{ "Slash" , Key. },
{ "ShiftRight", Key.RightShift },
{ "NumpadMultiply", Key.Multiply },
{ "AltLeft", Key.LeftAlt },
{ "Space", Key.Space },
{ "CapsLock", Key.CapsLock },
{ "F1", Key.F1 },
{ "F2", Key.F2 },
{ "F3", Key.F3 },
{ "F4", Key.F4 },
{ "F5", Key.F5 },
{ "F6", Key.F6 },
{ "F7", Key.F7 },
{ "F8", Key.F8 },
{ "F9", Key.F9 },
{ "F10", Key.F10 },
{ "NumLock", Key.NumLock },
{ "ScrollLock", Key.Scroll },
{ "Numpad7", Key.NumPad7 },
{ "Numpad8", Key.NumPad8 },
{ "Numpad9", Key.NumPad9 },
{ "NumpadSubtract", Key.Subtract },
{ "Numpad4", Key.NumPad4 },
{ "Numpad5", Key.NumPad5 },
{ "Numpad6", Key.NumPad6 },
{ "NumpadAdd", Key.Add },
{ "Numpad1", Key.NumPad1 },
{ "Numpad2", Key.NumPad2 },
{ "Numpad3", Key.NumPad3 },
{ "Numpad0", Key.NumPad0 },
{ "NumpadDecimal", Key.Decimal },
{ "Unidentified", Key.NoName },
//{ "IntlBackslash" , Key.bac },
{ "F11", Key.F11 },
{ "F12", Key.F12 },
//{ "IntlRo" , Key.Ro },
//{ "Unidentified" , Key. },
{ "Convert", Key.ImeConvert },
{ "KanaMode", Key.KanaMode },
{ "NonConvert", Key.ImeNonConvert },
//{ "Unidentified" , Key. },
{ "NumpadEnter", Key.Enter },
{ "ControlRight", Key.RightCtrl },
{ "NumpadDivide", Key.Divide },
{ "PrintScreen", Key.PrintScreen },
{ "AltRight", Key.RightAlt },
//{ "Unidentified" , Key. },
{ "Home", Key.Home },
{ "ArrowUp", Key.Up },
{ "PageUp", Key.PageUp },
{ "ArrowLeft", Key.Left },
{ "ArrowRight", Key.Right },
{ "End", Key.End },
{ "ArrowDown", Key.Down },
{ "PageDown", Key.PageDown },
{ "Insert", Key.Insert },
{ "Delete", Key.Delete },
//{ "Unidentified" , Key. },
{ "AudioVolumeMute", Key.VolumeMute },
{ "AudioVolumeDown", Key.VolumeDown },
{ "AudioVolumeUp", Key.VolumeUp },
//{ "NumpadEqual" , Key. },
{ "Pause", Key.Pause },
{ "NumpadComma", Key.OemComma }
};
}
}

18
src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs

@ -0,0 +1,18 @@
using System;
using System.Diagnostics;
using Avalonia.Rendering;
namespace Avalonia.Web
{
public class ManualTriggerRenderTimer : IRenderTimer
{
private static readonly Stopwatch s_sw = Stopwatch.StartNew();
public static ManualTriggerRenderTimer Instance { get; } = new();
public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed);
public event Action<TimeSpan>? Tick;
public bool RunsInBackground => false;
}
}

26
src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs

@ -0,0 +1,26 @@
using System.Collections.Generic;
using Avalonia.Skia;
namespace Avalonia.Web.Skia
{
public class BrowserSkiaGpu : ISkiaGpu
{
public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable<object> surfaces)
{
foreach (var surface in surfaces)
{
if (surface is BrowserSkiaSurface browserSkiaSurface)
{
return new BrowserSkiaGpuRenderTarget(browserSkiaSurface);
}
}
return null;
}
public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session)
{
return null;
}
}
}

36
src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs

@ -0,0 +1,36 @@
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession
{
private readonly SKSurface _surface;
public BrowserSkiaGpuRenderSession(BrowserSkiaSurface browserSkiaSurface, GRBackendRenderTarget renderTarget)
{
_surface = SKSurface.Create(browserSkiaSurface.Context, renderTarget, browserSkiaSurface.Origin, browserSkiaSurface.ColorType);
GrContext = browserSkiaSurface.Context;
ScaleFactor = browserSkiaSurface.Scaling;
SurfaceOrigin = browserSkiaSurface.Origin;
}
public void Dispose()
{
_surface.Flush();
_surface.Dispose();
}
public GRContext GrContext { get; }
public SKSurface SkSurface => _surface;
public double ScaleFactor { get; }
public GRSurfaceOrigin SurfaceOrigin { get; }
}
}

39
src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderTarget.cs

@ -0,0 +1,39 @@
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget
{
private readonly GRBackendRenderTarget _renderTarget;
private readonly BrowserSkiaSurface _browserSkiaSurface;
private readonly PixelSize _size;
public BrowserSkiaGpuRenderTarget(BrowserSkiaSurface browserSkiaSurface)
{
_size = browserSkiaSurface.Size;
var glFbInfo = new GRGlFramebufferInfo(browserSkiaSurface.GlInfo.FboId, browserSkiaSurface.ColorType.ToGlSizedFormat());
{
_browserSkiaSurface = browserSkiaSurface;
_renderTarget = new GRBackendRenderTarget(
(int)(browserSkiaSurface.Size.Width * browserSkiaSurface.Scaling),
(int)(browserSkiaSurface.Size.Height * browserSkiaSurface.Scaling),
browserSkiaSurface.GlInfo.Samples,
browserSkiaSurface.GlInfo.Stencils, glFbInfo);
}
}
public void Dispose()
{
_renderTarget.Dispose();
}
public ISkiaGpuRenderSession BeginRenderingSession()
{
return new BrowserSkiaGpuRenderSession(_browserSkiaSurface, _renderTarget);
}
public bool IsCorrupted => _browserSkiaSurface.Size != _size;
}
}

88
src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs

@ -0,0 +1,88 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Platform;
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaRasterSurface : IBrowserSkiaSurface, IFramebufferPlatformSurface, IDisposable
{
public SKColorType ColorType { get; set; }
public PixelSize Size { get; set; }
public double Scaling { get; set; }
private FramebufferData? _fbData;
private readonly Action<IntPtr, SKSizeI> _blitCallback;
private readonly Action _onDisposeAction;
public BrowserSkiaRasterSurface(
SKColorType colorType, PixelSize size, double scaling, Action<IntPtr, SKSizeI> blitCallback)
{
ColorType = colorType;
Size = size;
Scaling = scaling;
_blitCallback = blitCallback;
_onDisposeAction = Blit;
}
public void Dispose()
{
_fbData?.Dispose();
_fbData = null;
}
public ILockedFramebuffer Lock()
{
var bytesPerPixel = 4; // TODO: derive from ColorType
var dpi = Scaling * 96.0;
var width = (int)(Size.Width * Scaling);
var height = (int)(Size.Height * Scaling);
if (_fbData is null || _fbData?.Size.Width != width || _fbData?.Size.Height != height)
{
_fbData?.Dispose();
_fbData = new FramebufferData(width, height, bytesPerPixel);
}
var pixelFormat = ColorType.ToPixelFormat();
var data = _fbData.Value;
return new LockedFramebuffer(
data.Address, data.Size, data.RowBytes,
new Vector(dpi, dpi), pixelFormat, _onDisposeAction);
}
private void Blit()
{
if (_fbData != null)
{
var data = _fbData.Value;
_blitCallback(data.Address, new SKSizeI(data.Size.Width, data.Size.Height));
}
}
private readonly struct FramebufferData
{
public PixelSize Size { get; }
public int RowBytes { get; }
public IntPtr Address { get; }
public FramebufferData(int width, int height, int bytesPerPixel)
{
Size = new PixelSize(width, height);
RowBytes = width * bytesPerPixel;
Address = Marshal.AllocHGlobal(width * height * bytesPerPixel);
}
public void Dispose()
{
Marshal.FreeHGlobal(Address);
}
}
}
}

30
src/Web/Avalonia.Web/Skia/BrowserSkiaSurface.cs

@ -0,0 +1,30 @@
using Avalonia.Web.Interop;
using SkiaSharp;
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaSurface : IBrowserSkiaSurface
{
public BrowserSkiaSurface(GRContext context, GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin)
{
Context = context;
GlInfo = glInfo;
ColorType = colorType;
Size = size;
Scaling = scaling;
Origin = origin;
}
public SKColorType ColorType { get; set; }
public PixelSize Size { get; set; }
public GRContext Context { get; set; }
public GRSurfaceOrigin Origin { get; set; }
public double Scaling { get; set; }
public GLInfo GlInfo { get; set; }
}
}

9
src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs

@ -0,0 +1,9 @@
namespace Avalonia.Web.Skia
{
internal interface IBrowserSkiaSurface
{
public PixelSize Size { get; set; }
public double Scaling { get; set; }
}
}

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

@ -0,0 +1,88 @@
using System;
using System.IO;
using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Web.Storage;
[System.Runtime.Versioning.SupportedOSPlatform("browser")]
internal class BlobReadableStream : Stream
{
private JSObject? _jSReference;
private long _position;
private readonly long _length;
public BlobReadableStream(JSObject jsStreamReference)
{
_jSReference = jsStreamReference;
_position = 0;
_length = StreamHelper.ByteLength(JSReference);
}
private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream));
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => _length;
public override long Position
{
get => _position;
set => throw new NotSupportedException();
}
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin)
{
return _position = origin switch
{
SeekOrigin.Current => _position + offset,
SeekOrigin.End => _length + offset,
_ => offset
};
}
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count)
{
throw new InvalidOperationException("Browser supports only ReadAsync");
}
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> await ReadAsync(buffer.AsMemory(offset, count), cancellationToken);
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position);
var bytesRead = await StreamHelper.SliceAsync(JSReference, _position, numBytesToRead);
if (bytesRead.Length != numBytesToRead)
{
throw new EndOfStreamException("Failed to read the requested number of bytes from the stream.");
}
_position += bytesRead.Length;
bytesRead.CopyTo(buffer);
return bytesRead.Length;
}
protected override void Dispose(bool disposing)
{
if (_jSReference is { } jsReference)
{
_jSReference = null;
jsReference.Dispose();
}
}
}

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

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

@ -0,0 +1,124 @@
using System;
using System.IO;
using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Web.Storage;
[System.Runtime.Versioning.SupportedOSPlatform("browser")]
// Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream
internal sealed class WriteableStream : Stream
{
private JSObject? _jSReference;
// Unfortunatelly we can't read current length/position, so we need to keep it C#-side only.
private long _length, _position;
internal WriteableStream(JSObject jSReference, long initialLength)
{
_jSReference = jSReference;
_length = initialLength;
}
private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream));
public override bool CanRead => false;
public override bool CanSeek => true;
public override bool CanWrite => true;
public override long Length => _length;
public override long Position
{
get => _position;
set => Seek(_position, SeekOrigin.Begin);
}
public override void Flush()
{
// no-op
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
var position = origin switch
{
SeekOrigin.Current => _position + offset,
SeekOrigin.End => _length + offset,
_ => offset
};
StreamHelper.Seek(JSReference, position);
return position;
}
public override void SetLength(long value)
{
_length = value;
// See https://docs.w3cub.com/dom/filesystemwritablefilestream/truncate
// If the offset is smaller than the size, it remains unchanged. If the offset is larger than size, the offset is set to that size
if (_position > _length)
{
_position = _length;
}
StreamHelper.Truncate(JSReference, value);
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new InvalidOperationException("Browser supports only WriteAsync");
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
return new ValueTask(WriteAsyncInternal(buffer.ToArray(), cancellationToken));
}
private Task WriteAsyncInternal(byte[] buffer, CancellationToken _)
{
_position += buffer.Length;
return StreamHelper.WriteAsync(JSReference, buffer);
}
protected override void Dispose(bool disposing)
{
if (_jSReference is { } jsReference)
{
_jSReference = null;
try
{
_ = StreamHelper.CloseAsync(jsReference);
}
finally
{
jsReference.Dispose();
}
}
}
public override async ValueTask DisposeAsync()
{
if (_jSReference is { } jsReference)
{
_jSReference = null;
try
{
await StreamHelper.CloseAsync(jsReference);
}
finally
{
jsReference.Dispose();
}
}
}
}

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

48
src/Web/Avalonia.Web/WinStubs.cs

@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.IO;
using Avalonia.Platform;
#nullable enable
namespace Avalonia.Web
{
internal class IconLoaderStub : IPlatformIconLoader
{
private class IconStub : IWindowIconImpl
{
public void Save(Stream outputStream)
{
}
}
public IWindowIconImpl LoadIcon(string fileName) => new IconStub();
public IWindowIconImpl LoadIcon(Stream stream) => new IconStub();
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub();
}
internal class ScreenStub : IScreenImpl
{
public int ScreenCount => 1;
public IReadOnlyList<Screen> AllScreens { get; } =
new[] { new Screen(96, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) };
public Screen? ScreenFromPoint(PixelPoint point)
{
return ScreenHelper.ScreenFromPoint(point, AllScreens);
}
public Screen? ScreenFromRect(PixelRect rect)
{
return ScreenHelper.ScreenFromRect(rect, AllScreens);
}
public Screen? ScreenFromWindow(IWindowBaseImpl window)
{
return ScreenHelper.ScreenFromWindow(window, AllScreens);
}
}
}

105
src/Web/Avalonia.Web/WindowingPlatform.cs

@ -0,0 +1,105 @@
using System;
using System.Threading;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace Avalonia.Web
{
public class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface
{
private bool _signaled;
private static KeyboardDevice? s_keyboard;
public IWindowImpl CreateWindow() => throw new NotSupportedException();
IWindowImpl IWindowingPlatform.CreateEmbeddableWindow()
{
throw new NotImplementedException();
}
public ITrayIconImpl? CreateTrayIcon()
{
return null;
}
public static KeyboardDevice Keyboard => s_keyboard ??
throw new InvalidOperationException("BrowserWindowingPlatform not registered.");
public static void Register()
{
var instance = new BrowserWindowingPlatform();
s_keyboard = new KeyboardDevice();
AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToSingleton<ClipboardImpl>()
.Bind<ICursorFactory>().ToSingleton<CssCursorFactory>()
.Bind<IKeyboardDevice>().ToConstant(s_keyboard)
.Bind<IPlatformSettings>().ToConstant(instance)
.Bind<IPlatformThreadingInterface>().ToConstant(instance)
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(ManualTriggerRenderTimer.Instance)
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
}
public Size DoubleClickSize { get; } = new Size(2, 2);
public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500);
public Size TouchDoubleClickSize => new Size(16, 16);
public TimeSpan TouchDoubleClickTime => DoubleClickTime;
public void RunLoop(CancellationToken cancellationToken)
{
throw new NotSupportedException();
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
return GetRuntimePlatform()
.StartSystemTimer(interval, () =>
{
Dispatcher.UIThread.RunJobs(priority);
tick();
});
}
public void Signal(DispatcherPriority priority)
{
if (_signaled)
return;
_signaled = true;
IDisposable? disp = null;
disp = GetRuntimePlatform()
.StartSystemTimer(TimeSpan.FromMilliseconds(1),
() =>
{
_signaled = false;
disp?.Dispose();
Signaled?.Invoke(null);
});
}
public bool CurrentThreadIsLoopThread
{
get
{
return true; // Browser is single threaded.
}
}
public event Action<DispatcherPriority?>? Signaled;
private static IRuntimePlatform GetRuntimePlatform()
{
return AvaloniaLocator.Current.GetRequiredService<IRuntimePlatform>();
}
}
}

13
src/Web/Avalonia.Web/interop.js

@ -0,0 +1,13 @@
var LibraryExample = {
// Internal functions
$EXAMPLE: {
internal_func: function () {
}
},
InterceptGLObject: function () {
globalThis.AvaloniaGL = GL
}
}
autoAddDeps(LibraryExample, '$EXAMPLE')
mergeInto(LibraryManager.library, LibraryExample)

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

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

@ -0,0 +1,16 @@
require("esbuild").build({
entryPoints: [
"./modules/avalonia.ts",
"./modules/storage.ts"
],
outdir: "../wwwroot",
bundle: true,
minify: false,
format: "esm",
target: "es2016",
platform: "browser",
sourcemap: "linked",
loader: { ".ts": "ts" }
})
.then(() => console.log("⚡ Done"))
.catch(() => process.exit(1));

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

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

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

@ -0,0 +1,303 @@
interface SKGLViewInfo {
context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
fboId: number;
stencil: number;
sample: number;
depth: number;
}
type CanvasElement = {
Canvas: Canvas | undefined;
} & HTMLCanvasElement;
export class Canvas {
static elements: Map<string, HTMLCanvasElement>;
htmlCanvas: HTMLCanvasElement;
glInfo?: SKGLViewInfo;
renderFrameCallback: () => void;
renderLoopEnabled: boolean = false;
renderLoopRequest: number = 0;
newWidth?: number;
newHeight?: number;
public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null {
const view = Canvas.init(true, element, elementId, renderFrameCallback);
if (!view || !view.glInfo) {
return null;
}
return view.glInfo;
}
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): Canvas | null {
const htmlCanvas = element as CanvasElement;
if (!htmlCanvas) {
console.error("No canvas element was provided.");
return null;
}
if (!Canvas.elements) {
Canvas.elements = new Map<string, HTMLCanvasElement>();
}
Canvas.elements.set(elementId, element);
const view = new Canvas(useGL, element, renderFrameCallback);
htmlCanvas.Canvas = view;
return view;
}
public constructor(useGL: boolean, element: HTMLCanvasElement, renderFrameCallback: () => void) {
this.htmlCanvas = element;
this.renderFrameCallback = renderFrameCallback;
if (useGL) {
const ctx = Canvas.createWebGLContext(element);
if (!ctx) {
console.error("Failed to create WebGL context");
return;
}
const GL = (globalThis as any).AvaloniaGL;
// make current
GL.makeContextCurrent(ctx);
const GLctx = GL.currentContext.GLctx as WebGLRenderingContext;
// read values
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
this.glInfo = {
context: ctx,
fboId: fbo ? fbo.id : 0,
stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
depth: GLctx.getParameter(GLctx.DEPTH_BITS)
};
}
}
public setEnableRenderLoop(enable: boolean): void {
this.renderLoopEnabled = enable;
// either start the new frame or cancel the existing one
if (enable) {
// console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
this.requestAnimationFrame();
} else if (this.renderLoopRequest !== 0) {
window.cancelAnimationFrame(this.renderLoopRequest);
this.renderLoopRequest = 0;
}
}
public requestAnimationFrame(renderLoop?: boolean): void {
// optionally update the render loop
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) {
this.setEnableRenderLoop(renderLoop);
}
// skip because we have a render loop
if (this.renderLoopRequest !== 0) {
return;
}
// add the draw to the next frame
this.renderLoopRequest = window.requestAnimationFrame(() => {
if (this.glInfo) {
const GL = (globalThis as any).AvaloniaGL;
// make current
GL.makeContextCurrent(this.glInfo.context);
}
if (this.htmlCanvas.width !== this.newWidth) {
this.htmlCanvas.width = this.newWidth ?? 0;
}
if (this.htmlCanvas.height !== this.newHeight) {
this.htmlCanvas.height = this.newHeight ?? 0;
}
this.renderFrameCallback();
this.renderLoopRequest = 0;
// we may want to draw the next frame
if (this.renderLoopEnabled) {
this.requestAnimationFrame();
}
});
}
public setCanvasSize(width: number, height: number): void {
this.newWidth = width;
this.newHeight = height;
if (this.htmlCanvas.width !== this.newWidth) {
this.htmlCanvas.width = this.newWidth;
}
if (this.htmlCanvas.height !== this.newHeight) {
this.htmlCanvas.height = this.newHeight;
}
if (this.glInfo) {
const GL = (globalThis as any).AvaloniaGL;
// make current
GL.makeContextCurrent(this.glInfo.context);
}
}
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number): void {
const htmlCanvas = element as CanvasElement;
if (!htmlCanvas || !htmlCanvas.Canvas) {
return;
}
htmlCanvas.Canvas.setCanvasSize(width, height);
}
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean): void {
const htmlCanvas = element as CanvasElement;
if (!htmlCanvas || !htmlCanvas.Canvas) {
return;
}
htmlCanvas.Canvas.requestAnimationFrame(renderLoop);
}
static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext {
const contextAttributes = {
alpha: 1,
depth: 1,
stencil: 8,
antialias: 0,
premultipliedAlpha: 1,
preserveDrawingBuffer: 0,
preferLowPowerToHighPerformance: 0,
failIfMajorPerformanceCaveat: 0,
majorVersion: 2,
minorVersion: 0,
enableExtensionsByDefault: 1,
explicitSwapControl: 0,
renderViaOffscreenBackBuffer: 1
};
const GL = (globalThis as any).AvaloniaGL;
let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
if (!ctx && contextAttributes.majorVersion > 1) {
console.warn("Falling back to WebGL 1.0");
contextAttributes.majorVersion = 1;
contextAttributes.minorVersion = 0;
ctx = GL.createContext(htmlCanvas, contextAttributes);
}
return ctx;
}
}
type SizeWatcherElement = {
SizeWatcher: SizeWatcherInstance;
} & HTMLElement;
interface SizeWatcherInstance {
callback: (width: number, height: number) => void;
}
export class SizeWatcher {
static observer: ResizeObserver;
static elements: Map<string, HTMLElement>;
public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void): void {
if (!element || !callback) {
return;
}
SizeWatcher.init();
const watcherElement = element as SizeWatcherElement;
watcherElement.SizeWatcher = {
callback
};
SizeWatcher.elements.set(elementId, element);
SizeWatcher.observer.observe(element);
SizeWatcher.invoke(element);
}
public static unobserve(elementId: string): void {
if (!elementId || !SizeWatcher.observer) {
return;
}
const element = SizeWatcher.elements.get(elementId);
if (element) {
SizeWatcher.elements.delete(elementId);
SizeWatcher.observer.unobserve(element);
}
}
static init(): void {
if (SizeWatcher.observer) {
return;
}
SizeWatcher.elements = new Map<string, HTMLElement>();
SizeWatcher.observer = new ResizeObserver((entries) => {
for (const entry of entries) {
SizeWatcher.invoke(entry.target);
}
});
}
static invoke(element: Element): void {
const watcherElement = element as SizeWatcherElement;
const instance = watcherElement.SizeWatcher;
if (!instance || !instance.callback) {
return;
}
return instance.callback(element.clientWidth, element.clientHeight);
}
}
export class DpiWatcher {
static lastDpi: number;
static timerId: number;
static callback: (old: number, newdpi: number) => void;
public static getDpi(): number {
return window.devicePixelRatio;
}
public static start(callback: (old: number, newdpi: number) => void): number {
DpiWatcher.lastDpi = window.devicePixelRatio;
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
DpiWatcher.callback = callback;
return DpiWatcher.lastDpi;
}
public static stop(): void {
window.clearInterval(DpiWatcher.timerId);
}
static update(): void {
if (!DpiWatcher.callback) {
return;
}
const currentDpi = window.devicePixelRatio;
const lastDpi = DpiWatcher.lastDpi;
DpiWatcher.lastDpi = currentDpi;
if (Math.abs(lastDpi - currentDpi) > 0.001) {
DpiWatcher.callback(lastDpi, currentDpi);
}
}
}

149
src/Web/Avalonia.Web/webapp/modules/avalonia/caretHelper.ts

@ -0,0 +1,149 @@
// Based on https://github.com/component/textarea-caret-position/blob/master/index.js
export class CaretHelper {
public static getCaretCoordinates(
element: HTMLInputElement | HTMLTextAreaElement,
position: number,
options?: { debug: boolean }
) {
if (!isBrowser) {
throw new Error(
"textarea-caret-position#getCaretCoordinates should only be called in a browser"
);
}
const debug = options?.debug ?? false;
if (debug) {
const el = document.querySelector(
"#input-textarea-caret-position-mirror-div"
);
if (el) el.parentNode?.removeChild(el);
}
// The mirror div will replicate the textarea's style
const div = document.createElement("div");
div.id = "input-textarea-caret-position-mirror-div";
document.body.appendChild(div);
const style = div.style;
const computed = window.getComputedStyle
? window.getComputedStyle(element)
: ((element as any).currentStyle as CSSStyleDeclaration); // currentStyle for IE < 9
const isInput = element.nodeName === "INPUT";
// Default textarea styles
style.whiteSpace = "pre-wrap";
if (!isInput) style.wordWrap = "break-word"; // only for textarea-s
// Position off-screen
style.position = "absolute"; // required to return coordinates properly
if (!debug) style.visibility = "hidden"; // not 'display: none' because we want rendering
// Transfer the element's properties to the div
properties.forEach((prop: string) => {
if (isInput && prop === "lineHeight") {
// Special case for <input>s because text is rendered centered and line height may be != height
if (computed.boxSizing === "border-box") {
const height = parseInt(computed.height);
const outerHeight =
parseInt(computed.paddingTop) +
parseInt(computed.paddingBottom) +
parseInt(computed.borderTopWidth) +
parseInt(computed.borderBottomWidth);
const targetHeight = outerHeight + parseInt(computed.lineHeight);
if (height > targetHeight) {
style.lineHeight = `${height - outerHeight}px`;
} else if (height === targetHeight) {
style.lineHeight = computed.lineHeight;
} else {
style.lineHeight = "0";
}
} else {
style.lineHeight = computed.height;
}
} else {
(style as any)[prop] = (computed as any)[prop];
}
});
if (isFirefox) {
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
if (element.scrollHeight > parseInt(computed.height)) {
style.overflowY = "scroll";
}
} else {
style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
}
div.textContent = element.value.substring(0, position);
// The second special handling for input type="text" vs textarea:
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
if (isInput) div.textContent = div.textContent.replace(/\s/g, "\u00a0");
const span = document.createElement("span");
// Wrapping must be replicated *exactly*, including when a long word gets
// onto the next line, with whitespace at the end of the line before (#7).
// The *only* reliable way to do that is to copy the *entire* rest of the
// textarea's content into the <span> created at the caret position.
// For inputs, just '.' would be enough, but no need to bother.
span.textContent = element.value.substring(position) || "."; // || because a completely empty faux span doesn't render at all
div.appendChild(span);
const coordinates = {
top: span.offsetTop + parseInt(computed.borderTopWidth),
left: span.offsetLeft + parseInt(computed.borderLeftWidth),
height: parseInt(computed.lineHeight)
};
if (debug) {
span.style.backgroundColor = "#aaa";
} else {
document.body.removeChild(div);
}
return coordinates;
}
}
const properties = [
"direction", // RTL support
"boxSizing",
"width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
"height",
"overflowX",
"overflowY", // copy the scrollbar for IE
"borderTopWidth",
"borderRightWidth",
"borderBottomWidth",
"borderLeftWidth",
"borderStyle",
"paddingTop",
"paddingRight",
"paddingBottom",
"paddingLeft",
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
"fontStyle",
"fontVariant",
"fontWeight",
"fontStretch",
"fontSize",
"fontSizeAdjust",
"lineHeight",
"fontFamily",
"textAlign",
"textTransform",
"textIndent",
"textDecoration", // might not make a difference, but better be safe
"letterSpacing",
"wordSpacing",
"tabSize",
"MozTabSize"
];
const isBrowser = typeof window !== "undefined";
const isFirefox = isBrowser && (window as any).mozInnerScreenX != null;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save