diff --git a/.gitignore b/.gitignore index abf7674560..9b15011929 100644 --- a/.gitignore +++ b/.gitignore @@ -210,3 +210,6 @@ obj-Skia/ coc-settings.json .ccls-cache .ccls +*.map +src/Web/Avalonia.Web.Blazor/wwwroot/*.js +src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js diff --git a/.ncrunch/Avalonia.Win32.v3.ncrunchproject b/.ncrunch/Avalonia.Win32.v3.ncrunchproject index a764f36a24..bc2ee91007 100644 --- a/.ncrunch/Avalonia.Win32.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Win32.v3.ncrunchproject @@ -1,7 +1,7 @@  - ..\..\tools\MicroComGenerator\bin\Debug\netcoreapp3.1\**.* + ..\..\tools\MicroComGenerator\bin\Debug\net6.0\**.* MissingOrIgnoredProjectReference diff --git a/Avalonia.sln b/Avalonia.sln index 6c3c3a92b9..a5707d086f 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29102.190 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject @@ -109,8 +109,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationDemo", "sampl EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{A0CC0258-D18C-4AB3-854F-7101680FC3F9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsInteropTest", "samples\interop\WindowsInteropTest\WindowsInteropTest.csproj", "{C7A69145-60B6-4882-97D6-A3921DD43978}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderDemo", "samples\RenderDemo\RenderDemo.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}" @@ -224,11 +222,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Blazor", "src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj", "{25831348-EB2A-483E-9576-E8F6528674A5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Web", "samples\ControlCatalog.Web\ControlCatalog.Web.csproj", "{C08E9894-AA92-426E-BF56-033E262CAD3E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsInteropTest", "samples\interop\WindowsInteropTest\WindowsInteropTest.csproj", "{26A98DA1-D89D-4A95-8152-349F404DA2E2}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -1192,30 +1198,6 @@ Global {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhone.Build.0 = Release|Any CPU {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|Any CPU.Build.0 = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|iPhone.Build.0 = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|iPhone.Build.0 = Debug|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|Any CPU.Build.0 = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|iPhone.ActiveCfg = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|iPhone.Build.0 = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {F1FDC5B0-4654-416F-AE69-E3E9BBD87801}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {F1FDC5B0-4654-416F-AE69-E3E9BBD87801}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {F1FDC5B0-4654-416F-AE69-E3E9BBD87801}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -2020,30 +2002,6 @@ Global {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.Build.0 = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.Build.0 = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.Build.0 = Release|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.ActiveCfg = Release|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -2140,6 +2098,78 @@ Global {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhone.Build.0 = Release|Any CPU {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|iPhone.Build.0 = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Debug|iPhone.Build.0 = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Release|Any CPU.Build.0 = Release|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Release|iPhone.ActiveCfg = Release|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Release|iPhone.Build.0 = Release|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {25831348-EB2A-483E-9576-E8F6528674A5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|iPhone.Build.0 = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|iPhone.Build.0 = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|Any CPU.Build.0 = Release|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|iPhone.ActiveCfg = Release|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|iPhone.Build.0 = Release|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.AppStore|iPhone.Build.0 = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|iPhone.Build.0 = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|Any CPU.Build.0 = Release|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|iPhone.ActiveCfg = Release|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|iPhone.Build.0 = Release|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2176,7 +2206,6 @@ Global {F1381F98-4D24-409A-A6C5-1C5B1E08BB08} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {FBCAF3D0-2808-4934-8E96-3F607594517B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {A0CC0258-D18C-4AB3-854F-7101680FC3F9} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {C7A69145-60B6-4882-97D6-A3921DD43978} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {F1FDC5B0-4654-416F-AE69-E3E9BBD87801} = {9B9E3891-2366-4253-A952-D08BCEB71098} {29132311-1848-4FD6-AE0C-4FF841151BD3} = {9B9E3891-2366-4253-A952-D08BCEB71098} {7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} @@ -2201,6 +2230,9 @@ Global {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AEC9031E-06EA-4A9E-9E7F-7D7C719404DD} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {25831348-EB2A-483E-9576-E8F6528674A5} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} + {C08E9894-AA92-426E-BF56-033E262CAD3E} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/Documentation/build.md b/Documentation/build.md index 9f5436e68e..ddd38be887 100644 --- a/Documentation/build.md +++ b/Documentation/build.md @@ -1,6 +1,6 @@ # Windows -Avalonia requires at least Visual Studio 2019 and .NET Core SDK 3.1 to build on Windows. +Avalonia requires at least Visual Studio 2022 and dotnet 6 SDK 6.0.100 to build on all platforms. ### Clone the Avalonia repository @@ -16,7 +16,7 @@ Go to https://dotnet.microsoft.com/download/visual-studio-sdks and install the l ### Open in Visual Studio -Open the `Avalonia.sln` solution in Visual Studio 2019 or newer. The free Visual Studio Community edition works fine. Build and run the `Samples\ControlCatalog.Desktop` or `ControlCatalog.NetCore` project to see the sample application. +Open the `Avalonia.sln` solution in Visual Studio 2022 or newer. The free Visual Studio Community edition works fine. Build and run the `Samples\ControlCatalog.Desktop` or `ControlCatalog.NetCore` project to see the sample application. ### Troubleshooting @@ -43,27 +43,6 @@ Go to https://www.microsoft.com/net/core and follow the instructions for your OS The build process needs [Xcode](https://developer.apple.com/xcode/) to build the native library. Following the install instructions at the [Xcode](https://developer.apple.com/xcode/) website to properly install. -Linux operating systems ship with their own respective package managers however we will use [Homebrew](https://brew.sh/) to manage packages on macOS. To install follow the instructions [here](https://docs.brew.sh/Installation). - -### Install CastXML (pre Nov 2020) - -Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processing during the build process. The easiest way to install this is via the operating system's package managers, such as below. - -On macOS: -``` -brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb -``` - -On Debian based Linux (Debian, Ubuntu, Mint, etc): -``` -sudo apt install castxml -``` - -On Red Hat based Linux (Fedora, CentOS, RHEL, etc) using `yum` (`dnf` takes same arguments though): -``` -sudo yum install castxml -``` - ### Clone the Avalonia repository diff --git a/NuGet.Config b/NuGet.Config index 7a1f28bea7..e430390d39 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -5,5 +5,6 @@ + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index bc8d065137..903146cdd7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,3 +1,6 @@ +variables: + MSBuildEnableWorkloadResolver: 'false' + jobs: - job: Linux pool: @@ -9,9 +12,9 @@ jobs: version: 3.1.414 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 5.0.402' + displayName: 'Use .NET Core SDK 6.0.100' inputs: - version: 5.0.402 + version: 6.0.100 - task: CmdLine@2 displayName: 'Run Build' @@ -40,9 +43,9 @@ jobs: version: 3.1.414 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 5.0.402' + displayName: 'Use .NET Core SDK 6.0.100' inputs: - version: 5.0.402 + version: 6.0.100 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -56,7 +59,7 @@ jobs: inputs: script: | export PATH="`pwd`/sdk:$PATH" - cd src/tools/MicroComGenerator; dotnet run -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h + cd src/tools/MicroComGenerator; dotnet run -f net6.0 -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h - task: Xcode@5 inputs: @@ -100,7 +103,7 @@ jobs: - job: Windows pool: - vmImage: 'windows-2019' + vmImage: 'windows-2022' variables: SolutionDir: '$(Build.SourcesDirectory)' steps: @@ -110,9 +113,9 @@ jobs: version: 3.1.414 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 5.0.402' + displayName: 'Use .NET Core SDK 6.0.100' inputs: - version: 5.0.402 + version: 6.0.100 - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/build/ApiDiff.props b/build/ApiDiff.props index 666417addf..a7454b4a4f 100644 --- a/build/ApiDiff.props +++ b/build/ApiDiff.props @@ -4,9 +4,9 @@ $(PackageId) Avalonia - - - - - + + + + + diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 13419eb173..1346a1dafc 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,6 +1,7 @@  - - + + + diff --git a/build/MicroCom.targets b/build/MicroCom.targets index 49d2cdce72..1ed388f689 100644 --- a/build/MicroCom.targets +++ b/build/MicroCom.targets @@ -15,7 +15,7 @@ Inputs="@(AvnComIdl);$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/**/*.cs" Outputs="%(AvnComIdl.OutputFile)"> - diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index f2e7df36cd..4a75a18290 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,7 @@  - - + + + diff --git a/global.json b/global.json index 9f83c1ea1e..e3e652761c 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { - "sdk": { - "version": "5.0.402" - }, + "sdk": { + "version": "6.0.100" + }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", "MSBuild.Sdk.Extras": "2.0.54", diff --git a/native/Avalonia.Native/src/OSX/dnd.mm b/native/Avalonia.Native/src/OSX/dnd.mm index 294b8ee8ea..531bdcccfd 100644 --- a/native/Avalonia.Native/src/OSX/dnd.mm +++ b/native/Avalonia.Native/src/OSX/dnd.mm @@ -32,7 +32,7 @@ extern NSString* GetAvnCustomDataType() - (NSDragOperation)draggingSession:(nonnull NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context { - return NSDragOperationCopy; + return _operation; } - (AvnDndSource*) initWithOperation: (NSDragOperation)operation diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 094c1f7c93..9208848b4c 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -206,7 +206,11 @@ public: auto window = Window; Window = nullptr; - [window close]; + try{ + // Seems to throw sometimes on application exit. + [window close]; + } + catch(NSException*){} } return S_OK; @@ -724,6 +728,7 @@ private: if (cparent->WindowState() == Minimized) cparent->SetWindowState(Normal); + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; UpdateStyle(); @@ -1489,7 +1494,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent NSRect rect = NSZeroRect; rect.size = newSize; - NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved | NSTrackingEnabledDuringMouseDrag; + NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingEnabledDuringMouseDrag; _area = [[NSTrackingArea alloc] initWithRect:rect options:options owner:self userInfo:nullptr]; [self addTrackingArea:_area]; @@ -2398,11 +2403,18 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { case NSEventTypeLeftMouseDown: { - auto avnPoint = [AvnView toAvnPoint:[event locationInWindow]]; - auto point = [self translateLocalPoint:avnPoint]; - AvnVector delta; + AvnView* view = _parent->View; + NSPoint windowPoint = [event locationInWindow]; + NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil]; - _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + if (!NSPointInRect(viewPoint, view.bounds)) + { + auto avnPoint = [AvnView toAvnPoint:windowPoint]; + auto point = [self translateLocalPoint:avnPoint]; + AvnVector delta; + + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + } } break; diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index e08ffd0413..b28d3eb700 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -15,6 +15,7 @@ + @@ -36,10 +37,10 @@ - - - - + + MicroComGenerator\%(Filename)%(Extension) + + diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 3f9ccb04eb..d43a5c1624 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -88,6 +88,7 @@ $(IntermediateOutputPath)/Avalonia/references $(IntermediateOutputPath)/Avalonia/original.dll false + false - + + ## .NET Foundation diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index d898b737a9..2c6ff74e5e 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp3.1 + net6.0 diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 3c2d2ee359..2d4fc45171 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -2,7 +2,7 @@ WinExe - netcoreapp3.1 + net6.0 true diff --git a/samples/ControlCatalog.Web/App.razor b/samples/ControlCatalog.Web/App.razor new file mode 100644 index 0000000000..b941644e29 --- /dev/null +++ b/samples/ControlCatalog.Web/App.razor @@ -0,0 +1,10 @@ + + + + + + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/samples/ControlCatalog.Web/App.razor.cs b/samples/ControlCatalog.Web/App.razor.cs new file mode 100644 index 0000000000..a150824ac3 --- /dev/null +++ b/samples/ControlCatalog.Web/App.razor.cs @@ -0,0 +1,14 @@ +using Avalonia.Web.Blazor; + +namespace ControlCatalog.Web; + +public partial class App +{ + protected override void OnParametersSet() + { + WebAppBuilder.Configure() + .SetupWithSingleViewLifetime(); + + base.OnParametersSet(); + } +} diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj new file mode 100644 index 0000000000..96936f3000 --- /dev/null +++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj @@ -0,0 +1,38 @@ + + + net6.0 + enable + + + + + + + + + false + -O1 + false + + + + true + true + -O3 + -O3 + false + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog.Web/LinkerConfig.xml b/samples/ControlCatalog.Web/LinkerConfig.xml new file mode 100644 index 0000000000..5839a0fe03 --- /dev/null +++ b/samples/ControlCatalog.Web/LinkerConfig.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog.Web/Pages/Index.razor b/samples/ControlCatalog.Web/Pages/Index.razor new file mode 100644 index 0000000000..93ca07f9f1 --- /dev/null +++ b/samples/ControlCatalog.Web/Pages/Index.razor @@ -0,0 +1,5 @@ +@page "/" + +@using Avalonia.Web.Blazor + + diff --git a/samples/ControlCatalog.Web/Program.cs b/samples/ControlCatalog.Web/Program.cs new file mode 100644 index 0000000000..d1a7925813 --- /dev/null +++ b/samples/ControlCatalog.Web/Program.cs @@ -0,0 +1,29 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.Extensions.DependencyInjection; +using ControlCatalog.Web; + +public class Program +{ + public static async Task Main(string[] args) + { + await CreateHostBuilder(args).Build().RunAsync(); + } + + public static WebAssemblyHostBuilder CreateHostBuilder(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + + builder.RootComponents.Add("#app"); + + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + + return builder; + } +} + + + + diff --git a/samples/ControlCatalog.Web/Properties/launchSettings.json b/samples/ControlCatalog.Web/Properties/launchSettings.json new file mode 100644 index 0000000000..e4da60f7ca --- /dev/null +++ b/samples/ControlCatalog.Web/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:13961", + "sslPort": 44319 + } + }, + "profiles": { + "ControlCatalog.Web - IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "ControlCatalog.Web": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ControlCatalog.Web/Shared/MainLayout.razor b/samples/ControlCatalog.Web/Shared/MainLayout.razor new file mode 100644 index 0000000000..63fb17716c --- /dev/null +++ b/samples/ControlCatalog.Web/Shared/MainLayout.razor @@ -0,0 +1,7 @@ +@inherits LayoutComponentBase + +
+
+ @Body +
+
diff --git a/samples/ControlCatalog.Web/Shared/MainLayout.razor.css b/samples/ControlCatalog.Web/Shared/MainLayout.razor.css new file mode 100644 index 0000000000..43c355a47a --- /dev/null +++ b/samples/ControlCatalog.Web/Shared/MainLayout.razor.css @@ -0,0 +1,70 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +.main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + } + + .top-row a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row:not(.auth) { + display: none; + } + + .top-row.auth { + justify-content: space-between; + } + + .top-row a, .top-row .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .main > div { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/samples/ControlCatalog.Web/_Imports.razor b/samples/ControlCatalog.Web/_Imports.razor new file mode 100644 index 0000000000..04c7a8690e --- /dev/null +++ b/samples/ControlCatalog.Web/_Imports.razor @@ -0,0 +1,11 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using ControlCatalog.Web +@using ControlCatalog.Web.Shared +@using SkiaSharp diff --git a/samples/ControlCatalog.Web/wwwroot/css/app.css b/samples/ControlCatalog.Web/wwwroot/css/app.css new file mode 100644 index 0000000000..d2a8dc525c --- /dev/null +++ b/samples/ControlCatalog.Web/wwwroot/css/app.css @@ -0,0 +1,90 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + margin: 0; + height: 100vh; + overflow: hidden; + touch-action: none; +} + +a, .btn-link { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.canvas-container { + opacity:1; + background-color:#ccc; + position:fixed; + width:100%; + height:100%; + top:0px; + left:0px; + z-index:500; +} + +canvas +{ + opacity:1; + background-color:#ccc; + position:fixed; + width:100%; + height:100%; + top:0px; + left:0px; + z-index:500; +} + +#app, .page { + height: 100%; +} + +.overlay{ + opacity:0.0; + background-color:#ccc; + position:fixed; + width:100vw; + height:100vh; + top:0px; + left:0px; + z-index:1000; +} diff --git a/samples/ControlCatalog.Web/wwwroot/favicon.ico b/samples/ControlCatalog.Web/wwwroot/favicon.ico new file mode 100644 index 0000000000..da8d49ff9b Binary files /dev/null and b/samples/ControlCatalog.Web/wwwroot/favicon.ico differ diff --git a/samples/ControlCatalog.Web/wwwroot/index.html b/samples/ControlCatalog.Web/wwwroot/index.html new file mode 100644 index 0000000000..7ea600673a --- /dev/null +++ b/samples/ControlCatalog.Web/wwwroot/index.html @@ -0,0 +1,23 @@ + + + + + + + Avalonia Sample + + + + + +
Powered by Avalonia
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + diff --git a/samples/ControlCatalog.Web/wwwroot/js/app.js b/samples/ControlCatalog.Web/wwwroot/js/app.js new file mode 100644 index 0000000000..5f282702bb --- /dev/null +++ b/samples/ControlCatalog.Web/wwwroot/js/app.js @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index c22cf68b68..cd8bf78c88 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -16,11 +16,16 @@ Drag Me (custom) + - Drop some text or files here + DragDrop.AllowDrop="True" Name="CopyTarget"> + Drop some text or files here (Copy) + + + Drop some text or files here (Move) diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index 5a52dbe12b..ae1ec326cd 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -21,12 +21,12 @@ namespace ControlCatalog.Pages int textCount = 0; SetupDnd("Text", d => d.Set(DataFormats.Text, - $"Text was dragged {++textCount} times")); + $"Text was dragged {++textCount} times"), DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link); - SetupDnd("Custom", d => d.Set(CustomFormat, "Test123")); + SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"), DragDropEffects.Move); } - void SetupDnd(string suffix, Action factory, DragDropEffects effects = DragDropEffects.Copy) + void SetupDnd(string suffix, Action factory, DragDropEffects effects) { var dragMe = this.Find("DragMe" + suffix); var dragState = this.Find("DragState"+suffix); @@ -36,9 +36,12 @@ namespace ControlCatalog.Pages var dragData = new DataObject(); factory(dragData); - var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy); + var result = await DragDrop.DoDragDrop(e, dragData, effects); switch (result) { + case DragDropEffects.Move: + dragState.Text = "Data was moved"; + break; case DragDropEffects.Copy: dragState.Text = "Data was copied"; break; @@ -48,13 +51,22 @@ namespace ControlCatalog.Pages case DragDropEffects.None: dragState.Text = "The drag operation was canceled"; break; + default: + dragState.Text = "Unknown result"; + break; } } void DragOver(object sender, DragEventArgs e) { - // Only allow Copy or Link as Drop Operations. - e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link); + if (e.Source is Control c && c.Name == "MoveTarget") + { + e.DragEffects = e.DragEffects & (DragDropEffects.Move); + } + else + { + e.DragEffects = e.DragEffects & (DragDropEffects.Copy); + } // Only allow if the dragged data contains text or filenames. if (!e.Data.Contains(DataFormats.Text) @@ -65,6 +77,15 @@ namespace ControlCatalog.Pages void Drop(object sender, DragEventArgs e) { + if (e.Source is Control c && c.Name == "MoveTarget") + { + e.DragEffects = e.DragEffects & (DragDropEffects.Move); + } + else + { + e.DragEffects = e.DragEffects & (DragDropEffects.Copy); + } + if (e.Data.Contains(DataFormats.Text)) _DropState.Text = e.Data.GetText(); else if (e.Data.Contains(DataFormats.FileNames)) diff --git a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj index 86d762a5bc..9660d2a90d 100644 --- a/samples/PlatformSanityChecks/PlatformSanityChecks.csproj +++ b/samples/PlatformSanityChecks/PlatformSanityChecks.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj index cfedb7ad9e..c1d14cba26 100644 --- a/samples/Previewer/Previewer.csproj +++ b/samples/Previewer/Previewer.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp3.1 + net6.0 diff --git a/samples/RemoteDemo/RemoteDemo.csproj b/samples/RemoteDemo/RemoteDemo.csproj index 530cad805f..607222c2e2 100644 --- a/samples/RemoteDemo/RemoteDemo.csproj +++ b/samples/RemoteDemo/RemoteDemo.csproj @@ -1,7 +1,7 @@ Exe - netcoreapp3.1 + net6.0 diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index 0d33b4c111..eed6fa9e89 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp3.1 + net6.0 diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index 0c19440a1e..8f2812e048 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -2,7 +2,7 @@ WinExe - netcoreapp3.1 + net6.0 true diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index d898b737a9..2c6ff74e5e 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp3.1 + net6.0 diff --git a/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs index bc627f57ce..e6603a817b 100644 --- a/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs +++ b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs @@ -8,16 +8,28 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using Avalonia.Controls; +using Avalonia.Rendering; +using Avalonia.VisualTree; using ControlCatalog; namespace WindowsInteropTest { public partial class EmbedToWinFormsDemo : Form { + private readonly IRenderer _renderer; + public EmbedToWinFormsDemo() { InitializeComponent(); avaloniaHost.Content = new MainView(); + _renderer = ((TopLevel)avaloniaHost.Content.GetVisualRoot()).Renderer; + _renderer.Start(); + } + + protected override void OnClosed(EventArgs e) + { + _renderer.Stop(); + base.OnClosed(e); } } } diff --git a/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml.cs b/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml.cs index c7a23c22fc..e1476106ee 100644 --- a/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml.cs +++ b/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml.cs @@ -13,6 +13,7 @@ using System.Windows.Navigation; using System.Windows.Shapes; using Avalonia; using Avalonia.Controls; +using Avalonia.Rendering; using Avalonia.VisualTree; using ControlCatalog; using Window = System.Windows.Window; @@ -24,15 +25,16 @@ namespace WindowsInteropTest /// public partial class EmbedToWpfDemo : Window { + private IRenderer _renderer; public EmbedToWpfDemo() { InitializeComponent(); var view = new MainView(); - view.AttachedToVisualTree += delegate - { - ((TopLevel) view.GetVisualRoot()).AttachDevTools(); - }; Host.Content = view; + var tl = (TopLevel)view.GetVisualRoot(); + tl.AttachDevTools(); + _renderer = tl.Renderer; + _renderer.Start(); var btn = (Avalonia.Controls.Button) RightBtn.Content; btn.Click += delegate { @@ -40,5 +42,11 @@ namespace WindowsInteropTest }; } + + protected override void OnClosed(EventArgs e) + { + _renderer.Stop(); + base.OnClosed(e); + } } } diff --git a/samples/interop/WindowsInteropTest/Properties/AssemblyInfo.cs b/samples/interop/WindowsInteropTest/Properties/AssemblyInfo.cs deleted file mode 100644 index 087fdfe991..0000000000 --- a/samples/interop/WindowsInteropTest/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("WindowsInteropTest")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("WindowsInteropTest")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("c7a69145-60b6-4882-97d6-a3921dd43978")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj index 8394d7cb13..0c18a1f58b 100644 --- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -1,189 +1,19 @@ - - - + - Debug - AnyCPU - {C7A69145-60B6-4882-97D6-A3921DD43978} WinExe - Properties - WindowsInteropTest - WindowsInteropTest - v4.6.1 - 512 - true + net461 + + true + true - - x86 - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - Form - - - EmbedToWinFormsDemo.cs - - - EmbedToWpfDemo.xaml - - - Form - - - SelectorForm.cs - - - - - EmbedToWinFormsDemo.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - - - SelectorForm.cs - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - + - - {d211e587-d8bc-45b9-95a4-f297c8fa5200} - Avalonia.Animation - - - {b09b78d8-9b26-48b0-9149-d64a2f120f3f} - Avalonia.Base - - - {3278f3a9-9509-4a3f-a15b-bdc8b5bff632} - Avalonia.Controls.DataGrid - - - {d2221c82-4a25-4583-9b43-d791e3f6820c} - Avalonia.Controls - - - {799a7bb5-3c2c-48b6-85a7-406a12c420da} - Avalonia.DesignerSupport - - - {878fefe0-cd14-41cb-90b0-dbcb163e8f15} - Avalonia.DesktopRuntime - - - {7062ae20-5dcc-4442-9645-8195bdece63e} - Avalonia.Diagnostics - - - {62024b2d-53eb-4638-b26b-85eeaa54866e} - Avalonia.Input - - - {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} - Avalonia.Interactivity - - - {42472427-4774-4c81-8aff-9f27b8e31721} - Avalonia.Layout - - - {eb582467-6abb-43a1-b052-e981ba910e3a} - Avalonia.Visuals - - - {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} - Avalonia.Styling - - - {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} - Avalonia.Themes.Default - - - {3e53a01a-b331-47f3-b828-4a5717e77a24} - Avalonia.Markup.Xaml - - - {6417e941-21bc-467b-a771-0de389353ce6} - Avalonia.Markup - - - {7d2d3083-71dd-4cc9-8907-39a0d86fb322} - Avalonia.Skia - - - {3e908f67-5543-4879-a1dc-08eace79b3cd} - Avalonia.Direct2D1 - - - {cbc4ff2f-92d4-420b-be21-9fe0b930b04e} - Avalonia.Win32.Interop - - - {811a76cf-1cf6-440f-963b-bbe31bd72a82} - Avalonia.Win32 - + {d0a739b9-3c68-4ba6-a328-41606954b6bd} ControlCatalog - - - Designer - MSBuild:Compile - - + - - diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 172782c5a9..a4515db514 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -353,6 +353,12 @@ namespace Avalonia.Animation return new CompositeDisposable(subscriptions); } + /// + public Task RunAsync(Animatable control, IClock clock = null) + { + return RunAsync(control, clock, default); + } + /// public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default) { diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index d784227620..23afa76bf6 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -79,15 +79,15 @@ namespace Avalonia.Animation.Animators T oldValue, newValue; - if (firstKeyframe.isNeutral) - oldValue = neutralValue; + if (!firstKeyframe.isNeutral && firstKeyframe.Value is T firstKeyframeValue) + oldValue = firstKeyframeValue; else - oldValue = (T)firstKeyframe.Value; + oldValue = neutralValue; - if (lastKeyframe.isNeutral) - newValue = neutralValue; + if (!lastKeyframe.isNeutral && lastKeyframe.Value is T lastKeyframeValue) + newValue = lastKeyframeValue; else - newValue = (T)lastKeyframe.Value; + newValue = neutralValue; if (lastKeyframe.KeySpline != null) progress = lastKeyframe.KeySpline.GetSplineProgress(progress); diff --git a/src/Avalonia.Animation/ApiCompatBaseline.txt b/src/Avalonia.Animation/ApiCompatBaseline.txt index 58cb7830e7..973698f872 100644 --- a/src/Avalonia.Animation/ApiCompatBaseline.txt +++ b/src/Avalonia.Animation/ApiCompatBaseline.txt @@ -1,6 +1,5 @@ Compat issues with assembly Avalonia.Animation: -MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.Animation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' is present in the contract but not in the implementation. MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock, System.Threading.CancellationToken)' is present in the implementation but not in the contract. -Total Issues: 4 +Total Issues: 3 diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 93948e54ee..e8c84ffb21 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -247,7 +247,7 @@ namespace Avalonia.Data UnsetValueType _ => Unset, DoNothingType _ => DoNothing, BindingNotification n => n.ToBindingValue().Cast(), - _ => new BindingValue((T)value) + _ => new BindingValue((T?)value) }; } diff --git a/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs index 7ff0a8ceca..251dbb458d 100644 --- a/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs +++ b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs @@ -140,18 +140,9 @@ namespace Avalonia.Data.Converters ); } - Action action = null; - try - { - action = Expression - .Lambda>(body, parameter) - .Compile(); - } - catch (Exception ex) - { - throw ex; - } - return action; + return Expression + .Lambda>(body, parameter) + .Compile(); } static Func CreateCanExecute(object target @@ -170,7 +161,7 @@ namespace Avalonia.Data.Converters .Compile(); } - private static Expression? ConvertTarget(object? target, MethodInfo method) => + private static Expression ConvertTarget(object target, MethodInfo method) => target is null ? null : Expression.Convert(Expression.Constant(target), method.DeclaringType); internal class WeakPropertyChangedProxy diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index b85991fb77..62a9ed27be 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -1,9 +1,6 @@ using System; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; -using System.Threading; using Microsoft.Build.Framework; namespace Avalonia.Build.Tasks @@ -41,7 +38,7 @@ namespace Avalonia.Build.Tasks File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), ProjectDirectory, OutputPath, VerifyIl, outputImportance, (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, - EnableComInteropPatching, SkipXamlCompilation); + EnableComInteropPatching, SkipXamlCompilation, DebuggerLaunch); if (!res.Success) return false; if (!res.WrittenFile) @@ -87,5 +84,7 @@ namespace Avalonia.Build.Tasks public IBuildEngine BuildEngine { get; set; } public ITaskHost HostObject { get; set; } + + public bool DebuggerLaunch { get; set; } } } diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 508045dccb..593d79471e 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -1,13 +1,10 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Text; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; using Microsoft.Build.Framework; using Mono.Cecil; -using Avalonia.Utilities; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; using XamlX; @@ -44,16 +41,23 @@ namespace Avalonia.Build.Tasks string projectDirectory, string output, bool verifyIl, MessageImportance logImportance, string strongNameKey, bool patchCom, bool skipXamlCompilation) + { + return Compile(engine, input, references, projectDirectory, output, verifyIl, logImportance, strongNameKey, patchCom, skipXamlCompilation, debuggerLaunch:false); + } + + internal static CompileResult Compile(IBuildEngine engine, string input, string[] references, + string projectDirectory, + string output, bool verifyIl, MessageImportance logImportance, string strongNameKey, bool patchCom, bool skipXamlCompilation, bool debuggerLaunch) { var typeSystem = new CecilTypeSystem(references .Where(r => !r.ToLowerInvariant().EndsWith("avalonia.build.tasks.dll")) .Concat(new[] { input }), input); - + var asm = typeSystem.TargetAssemblyDefinition; if (!skipXamlCompilation) { - var compileRes = CompileCore(engine, typeSystem, projectDirectory, verifyIl, logImportance); + var compileRes = CompileCore(engine, typeSystem, projectDirectory, verifyIl, logImportance, debuggerLaunch); if (compileRes == null && !patchCom) return new CompileResult(true); if (compileRes == false) @@ -62,7 +66,7 @@ namespace Avalonia.Build.Tasks if (patchCom) ComInteropHelper.PatchAssembly(asm, typeSystem); - + var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols }; if (!string.IsNullOrWhiteSpace(strongNameKey)) writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey); @@ -70,13 +74,43 @@ namespace Avalonia.Build.Tasks asm.Write(output, writerParameters); return new CompileResult(true, true); - + } - + static bool? CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem, string projectDirectory, bool verifyIl, - MessageImportance logImportance) + MessageImportance logImportance + , bool debuggerLaunch = false) { + if (debuggerLaunch) + { + // According this https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debugger.launch?view=net-6.0#remarks + // documentation, on not windows platform Debugger.Launch() always return true without running a debugger. + if (System.Diagnostics.Debugger.Launch()) + { + // Set timeout at 1 minut. + var time = new System.Diagnostics.Stopwatch(); + var timeout = TimeSpan.FromMinutes(1); + time.Start(); + + // wait for the debugger to be attacked or timeout. + while (!System.Diagnostics.Debugger.IsAttached && time.Elapsed < timeout) + { + engine.LogMessage($"[PID:{System.Diagnostics.Process.GetCurrentProcess().Id}] Wating attach debugger. Elapsed {time.Elapsed}...", MessageImportance.High); + System.Threading.Thread.Sleep(100); + } + + time.Stop(); + if (time.Elapsed >= timeout) + { + engine.LogMessage("Wating attach debugger timeout.", MessageImportance.Normal); + } + } + else + { + engine.LogMessage("Debugging cancelled.", MessageImportance.Normal); + } + } var asm = typeSystem.TargetAssemblyDefinition; var emres = new EmbeddedResources(asm); var avares = new AvaloniaResources(asm, projectDirectory); diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 157bebe02b..df55d9e250 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -104,7 +104,7 @@ namespace Avalonia /// /// The application's focus manager. /// - public IFocusManager FocusManager + public IFocusManager? FocusManager { get; private set; @@ -116,7 +116,7 @@ namespace Avalonia /// /// The application's input manager. /// - public InputManager InputManager + public InputManager? InputManager { get; private set; @@ -175,7 +175,7 @@ namespace Avalonia /// - /// - /// - public IApplicationLifetime ApplicationLifetime { get; set; } + public IApplicationLifetime? ApplicationLifetime { get; set; } event Action> IGlobalStyles.GlobalStylesAdded { diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 2a42d99ac5..3a2fd68af5 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -122,12 +123,23 @@ namespace Avalonia.Controls.ApplicationLifetimes lifetimeEvents.ShutdownRequested += OnShutdownRequested; _cts = new CancellationTokenSource(); - MainWindow?.Show(); + + // Note due to a bug in the JIT we wrap this in a method, otherwise MainWindow + // gets stuffed into a local var and can not be GCed until after the program stops. + // this method never exits until program end. + ShowMainWindow(); + Dispatcher.UIThread.MainLoop(_cts.Token); Environment.ExitCode = _exitCode; return _exitCode; } + [MethodImpl(MethodImplOptions.NoInlining)] + private void ShowMainWindow() + { + MainWindow?.Show(); + } + public void Dispose() { if (_activeLifetime == this) diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 4cf7db74d9..2c8a7c0831 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -1903,6 +1903,11 @@ namespace Avalonia.Controls } internal void ProcessPageDownKey(bool shift) { + if (!shift) + { + OnNextClick(); + return; + } switch (DisplayMode) { case CalendarMode.Month: @@ -1927,6 +1932,11 @@ namespace Avalonia.Controls } internal void ProcessPageUpKey(bool shift) { + if (!shift) + { + OnPreviousClick(); + return; + } switch (DisplayMode) { case CalendarMode.Month: diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 4b903d056c..d78ddd4169 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property /// - private static readonly DirectProperty IsOpenProperty = + public static readonly DirectProperty IsOpenProperty = AvaloniaProperty.RegisterDirect(nameof(IsOpen), x => x.IsOpen); @@ -562,8 +562,12 @@ namespace Avalonia.Controls.Primitives return eventArgs.Cancel; } - internal static void SetPresenterClasses(IControl presenter, Classes classes) + internal static void SetPresenterClasses(IControl? presenter, Classes classes) { + if(presenter is null) + { + return; + } //Remove any classes no longer in use, ignoring pseudo classes for (int i = presenter.Classes.Count - 1; i >= 0; i--) { diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 7b06d3c868..0bead04982 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -387,7 +387,7 @@ namespace Avalonia.Controls parent = parent.Parent; } - _isEmbeddedInMenu = parent is IMenu; + _isEmbeddedInMenu = parent.FindLogicalAncestorOfType(true) != null; } protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index e361e7b736..5f82e28722 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; @@ -376,7 +377,10 @@ namespace Avalonia.Controls.Platform { if (item.IsSubMenuOpen) { - if (item.IsTopLevel) + // PointerPressed events may bubble from disabled items in sub-menus. In this case, + // keep the sub-menu open. + var popup = (e.Source as ILogical)?.FindLogicalAncestorOfType(); + if (item.IsTopLevel && popup == null) { CloseMenu(item); } diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index a5495fdfc9..6a95c2e047 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -85,7 +85,9 @@ namespace Avalonia.Controls.Platform public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread; public event Action Signaled; +#pragma warning disable CS0067 public event Action Tick; +#pragma warning restore CS0067 } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index e39f0b1e99..acd45ad29a 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls.Platform if (platform == null) { - throw new Exception("Could not CreateWindow(): IWindowingPlatform is not registered."); + throw new Exception("Could not CreateTrayIcon(): IWindowingPlatform is not registered."); } return s_designerMode ? null : platform.CreateTrayIcon(); @@ -45,7 +45,7 @@ namespace Avalonia.Controls.Platform throw new Exception("Could not CreateWindow(): IWindowingPlatform is not registered."); } - return s_designerMode ? (IWindowImpl)platform.CreateEmbeddableWindow() : platform.CreateWindow(); + return s_designerMode ? platform.CreateEmbeddableWindow() : platform.CreateWindow(); } public static IWindowImpl CreateEmbeddableWindow() diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 4c9b95c22d..ffab7f86d1 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Platform; @@ -93,8 +94,8 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty OverlayDismissEventPassThroughProperty = AvaloniaProperty.Register(nameof(OverlayDismissEventPassThrough)); - public static readonly DirectProperty OverlayInputPassThroughElementProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty OverlayInputPassThroughElementProperty = + AvaloniaProperty.RegisterDirect( nameof(OverlayInputPassThroughElement), o => o.OverlayInputPassThroughElement, (o, v) => o.OverlayInputPassThroughElement = v); @@ -138,7 +139,7 @@ namespace Avalonia.Controls.Primitives private bool _isOpen; private bool _ignoreIsOpenChanged; private PopupOpenState? _openState; - private IInputElement _overlayInputPassThroughElement; + private IInputElement? _overlayInputPassThroughElement; private Action? _popupHostChangedHandler; /// @@ -310,7 +311,7 @@ namespace Avalonia.Controls.Primitives /// Gets or sets an element that should receive pointer input events even when underneath /// the popup's overlay. /// - public IInputElement OverlayInputPassThroughElement + public IInputElement? OverlayInputPassThroughElement { get => _overlayInputPassThroughElement; set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); @@ -397,7 +398,7 @@ namespace Avalonia.Controls.Primitives _isOpenRequested = false; var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); - var handlerCleanup = new CompositeDisposable(5); + var handlerCleanup = new CompositeDisposable(7); popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup); @@ -425,14 +426,28 @@ namespace Avalonia.Controls.Primitives (x, handler) => x.Deactivated -= handler).DisposeWith(handlerCleanup); SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus, - (x, handler) => x.LostFocus += handler, - (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup); + (x, handler) => x.LostFocus += handler, + (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup); + + SubscribeToEventHandler>(window.PlatformImpl, WindowPositionChanged, + (x, handler) => x.PositionChanged += handler, + (x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup); + + if (placementTarget is Layoutable layoutTarget) + { + // If the placement target is moved, update the popup position + SubscribeToEventHandler(layoutTarget, PlacementTargetLayoutUpdated, + (x, handler) => x.LayoutUpdated += handler, + (x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup); + } } - else + else if (topLevel is PopupRoot parentPopupRoot) { - var parentPopupRoot = topLevel as PopupRoot; + SubscribeToEventHandler>(parentPopupRoot, ParentPopupPositionChanged, + (x, handler) => x.PositionChanged += handler, + (x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup); - if (parentPopupRoot?.Parent is Popup popup) + if (parentPopupRoot.Parent is Popup popup) { SubscribeToEventHandler>(popup, ParentClosed, (x, handler) => x.Closed += handler, @@ -797,6 +812,12 @@ namespace Avalonia.Controls.Primitives Close(); } + private void WindowPositionChanged(PixelPoint pp) => HandlePositionChange(); + + private void PlacementTargetLayoutUpdated(object src, EventArgs e) => HandlePositionChange(); + + private void ParentPopupPositionChanged(object src, PixelPointEventArgs e) => HandlePositionChange(); + private IgnoreIsOpenScope BeginIgnoringIsOpen() { return new IgnoreIsOpenScope(this); diff --git a/src/Avalonia.Controls/Remote/RemoteServer.cs b/src/Avalonia.Controls/Remote/RemoteServer.cs index 4f5a7cd311..419792f004 100644 --- a/src/Avalonia.Controls/Remote/RemoteServer.cs +++ b/src/Avalonia.Controls/Remote/RemoteServer.cs @@ -15,9 +15,6 @@ namespace Avalonia.Controls.Remote public EmbeddableRemoteServerTopLevelImpl(IAvaloniaRemoteTransportConnection transport) : base(transport) { } -#pragma warning disable 67 - public Action LostFocus { get; set; } - } public RemoteServer(IAvaloniaRemoteTransportConnection transport) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 6419981fb1..6af40b0281 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -331,16 +331,17 @@ namespace Avalonia.Controls } } - private void MoveToPoint(PointerPoint x) + private void MoveToPoint(PointerPoint posOnTrack) { var orient = Orientation == Orientation.Horizontal; - - var pointDen = orient ? _track.Bounds.Width : _track.Bounds.Height; - // Just add epsilon to avoid NaN in case 0/0 - pointDen += double.Epsilon; - - var pointNum = orient ? x.Position.X : x.Position.Y; - var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d); + var thumbLength = (orient + ? _track.Thumb.Bounds.Width + : _track.Thumb.Bounds.Height) + double.Epsilon; + var trackLength = (orient + ? _track.Bounds.Width + : _track.Bounds.Height) - thumbLength; + var trackPos = orient ? posOnTrack.Position.X : posOnTrack.Position.Y; + var logicalPos = MathUtilities.Clamp((trackPos - thumbLength * 0.5) / trackLength, 0.0d, 1.0d); var invert = orient ? IsDirectionReversed ? 1 : 0 : IsDirectionReversed ? 0 : 1; diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index 0e35c610b2..6a0d4e2023 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -129,14 +129,14 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly StyledProperty PaneProperty = - AvaloniaProperty.Register(nameof(Pane)); + public static readonly StyledProperty PaneProperty = + AvaloniaProperty.Register(nameof(Pane)); /// /// Defines the property. /// - public static readonly StyledProperty PaneTemplateProperty = - AvaloniaProperty.Register(nameof(PaneTemplate)); + public static readonly StyledProperty PaneTemplateProperty = + AvaloniaProperty.Register(nameof(PaneTemplate)); /// /// Defines the property @@ -267,7 +267,7 @@ namespace Avalonia.Controls /// /// Gets or sets the data template used to display the header content of the control. /// - public IDataTemplate? PaneTemplate + public IDataTemplate PaneTemplate { get => GetValue(PaneTemplateProperty); set => SetValue(PaneTemplateProperty, value); diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index e8122dd311..c5a729afae 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls public bool SupportsSurroundingText => false; public TextInputMethodSurroundingText SurroundingText => throw new NotSupportedException(); - public event EventHandler SurroundingTextChanged; + public event EventHandler SurroundingTextChanged { add { } remove { } } public string TextBeforeCursor => null; public string TextAfterCursor => null; diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 59edb6278a..9fbc2fc8b2 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -140,7 +140,7 @@ namespace Avalonia.Controls /// Gets or sets the parameter to pass to the property of a /// . /// - public object CommandParameter + public object? CommandParameter { get { return GetValue(CommandParameterProperty); } set { SetValue(CommandParameterProperty, value); } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 0ca28ca196..4c94b725ea 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -994,6 +994,7 @@ namespace Avalonia.Controls protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { + base.OnPropertyChanged(change); if (change.Property == SystemDecorationsProperty) { var typedNewValue = change.NewValue.GetValueOrDefault(); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index ee008efb04..5861d0452d 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -193,6 +193,12 @@ namespace Avalonia.Controls try { IsVisible = false; + + if (this is IFocusScope scope) + { + FocusManager.Instance?.RemoveFocusScope(scope); + } + base.HandleClosed(); } finally diff --git a/src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs b/src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs index 0448a5c05d..45a6c97954 100644 --- a/src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs @@ -59,7 +59,7 @@ namespace Avalonia.DesignerSupport.Remote remove { _onMessage -= value; } } - public event Action OnException; + public event Action OnException { add { } remove { } } public void Start() { UpdaterThread(); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 7b32e21fbd..9905aa4afc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -17,16 +17,16 @@ namespace Avalonia.Diagnostics.ViewModels internal class ControlDetailsViewModel : ViewModelBase, IDisposable { private readonly IVisual _control; - private IDictionary> _propertyIndex; + private IDictionary>? _propertyIndex; private PropertyViewModel? _selectedProperty; - private DataGridCollectionView _propertiesView; + private DataGridCollectionView? _propertiesView; private bool _snapshotStyles; private bool _showInactiveStyles; private string? _styleStatus; - private object _selectedEntity; + private object? _selectedEntity; private readonly Stack<(string Name,object Entry)> _selectedEntitiesStack = new(); - private string _selectedEntityName; - private string _selectedEntityType; + private string? _selectedEntityName; + private string? _selectedEntityType; public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) { @@ -117,7 +117,7 @@ namespace Avalonia.Diagnostics.ViewModels public TreePageViewModel TreePage { get; } - public DataGridCollectionView PropertiesView + public DataGridCollectionView? PropertiesView { get => _propertiesView; private set => RaiseAndSetIfChanged(ref _propertiesView, value); @@ -127,7 +127,7 @@ namespace Avalonia.Diagnostics.ViewModels public ObservableCollection PseudoClasses { get; } - public object SelectedEntity + public object? SelectedEntity { get => _selectedEntity; set @@ -137,7 +137,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public string SelectedEntityName + public string? SelectedEntityName { get => _selectedEntityName; set @@ -147,7 +147,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public string SelectedEntityType + public string? SelectedEntityType { get => _selectedEntityType; set @@ -270,7 +270,7 @@ namespace Avalonia.Diagnostics.ViewModels private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { - if (_propertyIndex.TryGetValue(e.Property, out var properties)) + if (_propertyIndex is { } && _propertyIndex.TryGetValue(e.Property, out var properties)) { foreach (var property in properties) { @@ -284,6 +284,7 @@ namespace Avalonia.Diagnostics.ViewModels private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName != null + && _propertyIndex is { } && _propertyIndex.TryGetValue(e.PropertyName, out var properties)) { foreach (var property in properties) @@ -402,7 +403,7 @@ namespace Avalonia.Diagnostics.ViewModels var selectedProperty = SelectedProperty; var selectedEntity = SelectedEntity; var selectedEntityName = SelectedEntityName; - if (selectedProperty == null) + if (selectedEntity == null || selectedProperty == null) return; object? property; @@ -419,7 +420,7 @@ namespace Avalonia.Diagnostics.ViewModels ?.GetValue(selectedEntity); } if (property == null) return; - _selectedEntitiesStack.Push((Name:selectedEntityName,Entry:selectedEntity)); + _selectedEntitiesStack.Push((Name:selectedEntityName!,Entry:selectedEntity)); NavigateToProperty(property, selectedProperty.Name); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 4b18cf414a..27b28f35fc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -15,7 +15,7 @@ namespace Avalonia.Diagnostics.ViewModels Nodes = nodes; PropertiesFilter = new FilterViewModel(); - PropertiesFilter.RefreshFilter += (s, e) => Details?.PropertiesView.Refresh(); + PropertiesFilter.RefreshFilter += (s, e) => Details?.PropertiesView?.Refresh(); SettersFilter = new FilterViewModel(); SettersFilter.RefreshFilter += (s, e) => Details?.UpdateStyleFilters(); diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 9e426688d8..206c24ad5e 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -413,10 +413,10 @@ namespace Avalonia.FreeDesktop #region Events private event Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> - ItemsPropertiesUpdated; + ItemsPropertiesUpdated { add { } remove { } } private event Action<(uint revision, int parent)> LayoutUpdated; - private event Action<(int id, uint timestamp)> ItemActivationRequested; - private event Action PropertiesChanged; + private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } } + private event Action PropertiesChanged { add { } remove { } } async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action onError) { diff --git a/src/Avalonia.Input/ApiCompatBaseline.txt b/src/Avalonia.Input/ApiCompatBaseline.txt index 98eb8598d8..270c5305e5 100644 --- a/src/Avalonia.Input/ApiCompatBaseline.txt +++ b/src/Avalonia.Input/ApiCompatBaseline.txt @@ -3,6 +3,7 @@ MembersMustExist : Member 'public Avalonia.Platform.IPlatformHandle Avalonia.Inp MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.RightTappedEvent' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.TappedEvent' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.IFocusManager.RemoveFocusScope(Avalonia.Input.IFocusScope)' is present in the implementation but not in the contract. MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.InputElement.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.InputElement.TappedEvent' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. @@ -10,4 +11,4 @@ MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_Tapped(Sy MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_Tapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. TypesMustExist : Type 'Avalonia.Platform.IStandardCursorFactory' does not exist in the implementation but it does exist in the contract. -Total Issues: 11 +Total Issues: 12 diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 1432092ba1..2efd33df1d 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -162,6 +162,22 @@ namespace Avalonia.Input Focus(e); } + public void RemoveFocusScope(IFocusScope scope) + { + scope = scope ?? throw new ArgumentNullException(nameof(scope)); + + if (_focusScopes.TryGetValue(scope, out _)) + { + SetFocusedElement(scope, null); + _focusScopes.Remove(scope); + } + + if (Scope == scope) + { + Scope = null; + } + } + public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope; /// diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 8d74001309..639b4ef117 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -30,7 +30,7 @@ namespace Avalonia.Input "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - private static WeakReference s_lastPress = new WeakReference(null); + private static readonly WeakReference s_lastPress = new WeakReference(null); #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. static Gestures() @@ -86,16 +86,15 @@ namespace Avalonia.Input #pragma warning restore CS0618 // Type or member is obsolete if (clickCount <= 1) { - s_lastPress = new WeakReference(ev.Source); + s_lastPress.SetTarget(ev.Source); } - else if (s_lastPress != null && clickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) + else if (clickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); } } - } } diff --git a/src/Avalonia.Input/ICommandSource.cs b/src/Avalonia.Input/ICommandSource.cs index eed71759d5..410b3a2e47 100644 --- a/src/Avalonia.Input/ICommandSource.cs +++ b/src/Avalonia.Input/ICommandSource.cs @@ -1,5 +1,5 @@ using System.Windows.Input; - +#nullable enable namespace Avalonia.Input { /// @@ -12,13 +12,13 @@ namespace Avalonia.Input /// Classes that implement this interface should enable or disable based on the command's CanExecute return value. /// The property may be implemented as read-write if desired. /// - ICommand Command { get; } + ICommand? Command { get; } /// /// The parameter that will be passed to the command when executing the command. /// The property may be implemented as read-write if desired. /// - object CommandParameter { get; } + object? CommandParameter { get; } /// diff --git a/src/Avalonia.Input/IFocusManager.cs b/src/Avalonia.Input/IFocusManager.cs index e1b5087c3d..2510479a8e 100644 --- a/src/Avalonia.Input/IFocusManager.cs +++ b/src/Avalonia.Input/IFocusManager.cs @@ -35,5 +35,13 @@ namespace Avalonia.Input /// when it activates, e.g. when a Window is activated. /// void SetFocusScope(IFocusScope scope); + + /// + /// Notifies the focus manager that a focus scope has been removed. + /// + /// The focus scope to be removed. + /// This should not be called by client code. It is called by an + /// when it deactivates or closes, e.g. when a Window is closed. + void RemoveFocusScope(IFocusScope scope); } } diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index b5ab7b4da1..26ebf86857 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -632,7 +632,7 @@ namespace Avalonia.Input } else if (change.Property == IsKeyboardFocusWithinProperty) { - PseudoClasses.Set(":focus-within", _isKeyboardFocusWithin); + PseudoClasses.Set(":focus-within", change.NewValue.GetValueOrDefault()); } } diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 4431e108ed..1582f794ae 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -44,7 +44,7 @@ namespace Avalonia.Native public bool IsNativeMenuExported => _exported; - public event EventHandler OnIsNativeMenuExportedChanged; + public event EventHandler OnIsNativeMenuExportedChanged { add { } remove { } } public void SetNativeMenu(NativeMenu menu) { diff --git a/src/Avalonia.Styling/Styling/PropertySetterInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs index 1c3055fed6..bb0e342df6 100644 --- a/src/Avalonia.Styling/Styling/PropertySetterInstance.cs +++ b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs @@ -16,14 +16,14 @@ namespace Avalonia.Styling private readonly IStyleable _target; private readonly StyledPropertyBase? _styledProperty; private readonly DirectPropertyBase? _directProperty; - private readonly T _value; + private readonly T? _value; private IDisposable? _subscription; private bool _isActive; public PropertySetterInstance( IStyleable target, StyledPropertyBase property, - T value) + T? value) { _target = target; _styledProperty = property; @@ -57,7 +57,7 @@ namespace Avalonia.Styling { if (_styledProperty is object) { - _subscription = _target.SetValue(_styledProperty, _value, BindingPriority.Style); + _subscription = _target.SetValue(_styledProperty!, _value, BindingPriority.Style); } else { diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 1f3d6335a9..4c94ea02dd 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -101,7 +101,7 @@ namespace Avalonia.Styling data.result = new PropertySetterInstance( data.target, property, - (T)data.value); + (T?)data.value); } } @@ -128,7 +128,7 @@ namespace Avalonia.Styling data.result = new PropertySetterInstance( data.target, property, - (T)data.value); + (T)data.value!); } } diff --git a/src/Avalonia.Themes.Default/Expander.xaml b/src/Avalonia.Themes.Default/Expander.xaml index 7df65677b6..e72ddea163 100644 --- a/src/Avalonia.Themes.Default/Expander.xaml +++ b/src/Avalonia.Themes.Default/Expander.xaml @@ -15,7 +15,7 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> - + - + - + - + - + - + - + + /// Appends another matrix as post-multiplication operation. + /// Equivalent to this * value; + /// + /// A matrix. + /// Post-multiplied matrix. + public Matrix Append(Matrix value) + { + return this * value; + } + + /// + /// Prpends another matrix as pre-multiplication operation. + /// Equivalent to value * this; + /// + /// A matrix. + /// Pre-multiplied matrix. + public Matrix Prepend(Matrix value) + { + return value * this; + } + /// /// Calculates the determinant for this matrix. /// diff --git a/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs index 742bb9c804..76c2deef3b 100644 --- a/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs +++ b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs @@ -18,11 +18,11 @@ namespace Avalonia.Media.Transformation public static Matrix ComposeTransform(Matrix.Decomposed decomposed) { // According to https://www.w3.org/TR/css-transforms-1/#recomposing-to-a-2d-matrix - - return Matrix.CreateTranslation(decomposed.Translate) * - Matrix.CreateRotation(decomposed.Angle) * - Matrix.CreateSkew(decomposed.Skew.X, decomposed.Skew.Y) * - Matrix.CreateScale(decomposed.Scale); + return Matrix.Identity + .Prepend(Matrix.CreateTranslation(decomposed.Translate)) + .Prepend(Matrix.CreateRotation(decomposed.Angle)) + .Prepend(Matrix.CreateSkew(decomposed.Skew.X, decomposed.Skew.Y)) + .Prepend(Matrix.CreateScale(decomposed.Scale)); } public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progress) diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs index 36f5dd98f1..13a24cd523 100644 --- a/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs +++ b/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs @@ -86,6 +86,8 @@ namespace Avalonia.Media.Transformation if (fromIdentity && toIdentity) { + result.Matrix = Matrix.Identity; + return true; } @@ -179,7 +181,8 @@ namespace Avalonia.Media.Transformation } case OperationType.Identity: { - // Do nothing. + result.Matrix = Matrix.Identity; + break; } } diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 52427c4ae6..88fbd290e6 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -20,6 +20,7 @@ namespace Avalonia.Rendering { private readonly IVisual _root; private readonly IRenderRoot _renderRoot; + private bool _updateTransformedBounds = true; private IRenderTarget _renderTarget; /// @@ -34,6 +35,13 @@ namespace Avalonia.Rendering _renderRoot = root as IRenderRoot; } + private ImmediateRenderer(IVisual root, bool updateTransformedBounds) + { + _root = root ?? throw new ArgumentNullException(nameof(root)); + _renderRoot = root as IRenderRoot; + _updateTransformedBounds = updateTransformedBounds; + } + /// public bool DrawFps { get; set; } @@ -98,7 +106,7 @@ namespace Avalonia.Rendering /// The render target. public static void Render(IVisual visual, IRenderTarget target) { - using (var renderer = new ImmediateRenderer(visual)) + using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) using (var context = new DrawingContext(target.CreateDrawingContext(renderer))) { renderer.Render(context, visual, visual.Bounds); @@ -112,7 +120,7 @@ namespace Avalonia.Rendering /// The drawing context. public static void Render(IVisual visual, DrawingContext context) { - using (var renderer = new ImmediateRenderer(visual)) + using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) { renderer.Render(context, visual, visual.Bounds); } @@ -193,6 +201,12 @@ namespace Avalonia.Rendering Render(new DrawingContext(context), visual, visual.Bounds); } + internal static void Render(IVisual visual, DrawingContext context, bool updateTransformedBounds) + { + using var renderer = new ImmediateRenderer(visual, updateTransformedBounds); + renderer.Render(context, visual, visual.Bounds); + } + private static void ClearTransformedBounds(IVisual visual) { foreach (var e in visual.GetSelfAndVisualDescendants()) @@ -308,7 +322,8 @@ namespace Avalonia.Rendering new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform); #pragma warning restore 0618 - visual.TransformedBounds = transformed; + if (_updateTransformedBounds) + visual.TransformedBounds = transformed; foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)) { @@ -321,7 +336,7 @@ namespace Avalonia.Rendering : clipRect; Render(context, child, childClipRect); } - else + else if (_updateTransformedBounds) { ClearTransformedBounds(child); } @@ -329,7 +344,7 @@ namespace Avalonia.Rendering } } - if (!visual.IsVisible) + if (!visual.IsVisible && _updateTransformedBounds) { ClearTransformedBounds(visual); } diff --git a/src/Avalonia.X11/ICELib.cs b/src/Avalonia.X11/ICELib.cs new file mode 100644 index 0000000000..8ef21dd000 --- /dev/null +++ b/src/Avalonia.X11/ICELib.cs @@ -0,0 +1,60 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.X11 +{ + internal static class ICELib + { + private const string LibIce = "libICE.so.6"; + + [DllImport(LibIce, CallingConvention = CallingConvention.StdCall)] + public static extern int IceAddConnectionWatch( + IntPtr watchProc, + IntPtr clientData + ); + + [DllImport(LibIce, CallingConvention = CallingConvention.StdCall)] + public static extern void IceRemoveConnectionWatch( + IntPtr watchProc, + IntPtr clientData + ); + + [DllImport(LibIce, CallingConvention = CallingConvention.StdCall)] + public static extern IceProcessMessagesStatus IceProcessMessages( + IntPtr iceConn, + out IntPtr replyWait, + out bool replyReadyRet + ); + + [DllImport(LibIce, CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr IceSetErrorHandler( + IntPtr handler + ); + + [DllImport(LibIce, CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr IceSetIOErrorHandler( + IntPtr handler + ); + + public enum IceProcessMessagesStatus + { + IceProcessMessagesIoError = 1 + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void IceErrorHandler( + IntPtr iceConn, + bool swap, + int offendingMinorOpcode, + ulong offendingSequence, + int errorClass, + int severity, + IntPtr values + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void IceIOErrorHandler( + IntPtr iceConn + ); + } +} diff --git a/src/Avalonia.X11/SMLib.cs b/src/Avalonia.X11/SMLib.cs new file mode 100644 index 0000000000..6ffcad60bf --- /dev/null +++ b/src/Avalonia.X11/SMLib.cs @@ -0,0 +1,133 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.X11 +{ + internal static unsafe class SMLib + { + private const string LibSm = "libSM.so.6"; + + [DllImport(LibSm, CharSet = CharSet.Ansi)] + public static extern IntPtr SmcOpenConnection( + [MarshalAs(UnmanagedType.LPWStr)] string networkId, + IntPtr content, + int xsmpMajorRev, + int xsmpMinorRev, + ulong mask, + ref SmcCallbacks callbacks, + [MarshalAs(UnmanagedType.LPWStr)] [Out] + out string previousId, + [MarshalAs(UnmanagedType.LPWStr)] [Out] + out string clientIdRet, + int errorLength, + [Out] char[] errorStringRet); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern int SmcCloseConnection( + IntPtr smcConn, + int count, + string[] reasonMsgs + ); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern void SmcSaveYourselfDone( + IntPtr smcConn, + bool success + ); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern int SmcInteractRequest( + IntPtr smcConn, + SmDialogValue dialogType, + IntPtr interactProc, + IntPtr clientData + ); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern void SmcInteractDone( + IntPtr smcConn, + bool success + ); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr SmcGetIceConnection( + IntPtr smcConn + ); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr SmcSetErrorHandler( + IntPtr handler + ); + + public enum SmDialogValue + { + SmDialogError = 0 + } + + [StructLayout(LayoutKind.Sequential)] + public struct SmcCallbacks + { + public IntPtr SaveYourself; + private readonly IntPtr Unused0; + public IntPtr Die; + private readonly IntPtr Unused1; + public IntPtr SaveComplete; + private readonly IntPtr Unused2; + public IntPtr ShutdownCancelled; + private readonly IntPtr Unused3; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void IceWatchProc( + IntPtr iceConn, + IntPtr clientData, + bool opening, + IntPtr* watchData + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcDieProc( + IntPtr smcConn, + IntPtr clientData + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcInteractProc( + IntPtr smcConn, + IntPtr clientData + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcSaveCompleteProc( + IntPtr smcConn, + IntPtr clientData + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcSaveYourselfProc( + IntPtr smcConn, + IntPtr clientData, + int saveType, + bool shutdown, + int interactStyle, + bool fast + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcShutdownCancelledProc( + IntPtr smcConn, + IntPtr clientData + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcErrorHandler( + IntPtr smcConn, + bool swap, + int offendingMinorOpcode, + ulong offendingSequence, + int errorClass, + int severity, + IntPtr values + ); + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 8ff3b4f5e0..a18f91d301 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -80,7 +80,8 @@ namespace Avalonia.X11 .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) .Bind().ToConstant(new GtkSystemDialog()) - .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()); + .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) + .Bind().ToConstant(new X11PlatformLifetimeEvents(this)); X11Screens = Avalonia.X11.X11Screens.Init(this); Screens = new X11Screens(X11Screens); @@ -230,7 +231,19 @@ namespace Avalonia /// on their input devices by using sequences of characters or mouse operations that are natively available on their input devices. /// public bool? EnableIme { get; set; } - + + /// + /// Determines whether to enable support for the + /// X Session Management Protocol. + /// + /// + /// X Session Management Protocol is a standard implemented on most + /// Linux systems that uses Xorg. This enables apps to control how they + /// can control and/or cancel the pending shutdown requested by the user. + /// + public bool EnableSessionManagement { get; set; } = + Environment.GetEnvironmentVariable("AVALONIA_X11_USE_SESSION_MANAGEMENT") != "0"; + public IList GlProfiles { get; set; } = new List { new GlVersion(GlProfileType.OpenGL, 4, 0), diff --git a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs new file mode 100644 index 0000000000..06e1d8879f --- /dev/null +++ b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs @@ -0,0 +1,259 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Logging; +using Avalonia.Platform; +using Avalonia.Threading; + +namespace Avalonia.X11 +{ + internal unsafe class X11PlatformLifetimeEvents : IDisposable, IPlatformLifetimeEventsImpl + { + private readonly AvaloniaX11Platform _platform; + private const ulong SmcSaveYourselfProcMask = 1L; + private const ulong SmcDieProcMask = 2L; + private const ulong SmcSaveCompleteProcMask = 4L; + private const ulong SmcShutdownCancelledProcMask = 8L; + + private static readonly ConcurrentDictionary s_nativeToManagedMapper = + new ConcurrentDictionary(); + + private static readonly SMLib.SmcSaveYourselfProc s_saveYourselfProcDelegate = SmcSaveYourselfHandler; + private static readonly SMLib.SmcDieProc s_dieDelegate = SmcDieHandler; + + private static readonly SMLib.SmcShutdownCancelledProc + s_shutdownCancelledDelegate = SmcShutdownCancelledHandler; + + private static readonly SMLib.SmcSaveCompleteProc s_saveCompleteDelegate = SmcSaveCompleteHandler; + private static readonly SMLib.SmcInteractProc s_smcInteractDelegate = StaticInteractHandler; + private static readonly SMLib.SmcErrorHandler s_smcErrorHandlerDelegate = StaticErrorHandler; + private static readonly ICELib.IceErrorHandler s_iceErrorHandlerDelegate = StaticErrorHandler; + private static readonly ICELib.IceIOErrorHandler s_iceIoErrorHandlerDelegate = StaticIceIOErrorHandler; + private static readonly SMLib.IceWatchProc s_iceWatchProcDelegate = IceWatchHandler; + + private static SMLib.SmcCallbacks s_callbacks = new SMLib.SmcCallbacks() + { + ShutdownCancelled = Marshal.GetFunctionPointerForDelegate(s_shutdownCancelledDelegate), + Die = Marshal.GetFunctionPointerForDelegate(s_dieDelegate), + SaveYourself = Marshal.GetFunctionPointerForDelegate(s_saveYourselfProcDelegate), + SaveComplete = Marshal.GetFunctionPointerForDelegate(s_saveCompleteDelegate) + }; + + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private readonly IntPtr _currentIceConn; + private readonly IntPtr _currentSmcConn; + + private bool _saveYourselfPhase; + + internal X11PlatformLifetimeEvents(AvaloniaX11Platform platform) + { + _platform = platform; + + if (ICELib.IceAddConnectionWatch( + Marshal.GetFunctionPointerForDelegate(s_iceWatchProcDelegate), + IntPtr.Zero) == 0) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, + "SMLib was unable to add an ICE connection watcher."); + return; + } + + var errorBuf = new char[255]; + + var smcConn = SMLib.SmcOpenConnection(null!, + IntPtr.Zero, 1, 0, + SmcSaveYourselfProcMask | + SmcSaveCompleteProcMask | + SmcShutdownCancelledProcMask | + SmcDieProcMask, + ref s_callbacks, + out _, + out _, + errorBuf.Length, + errorBuf); + + if (smcConn == IntPtr.Zero) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, + $"SMLib/ICELib reported a new error: {new string(errorBuf)}"); + return; + } + + if (!s_nativeToManagedMapper.TryAdd(smcConn, this)) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, + "SMLib was unable to add this instance to the native to managed map."); + return; + } + + _ = SMLib.SmcSetErrorHandler(Marshal.GetFunctionPointerForDelegate(s_smcErrorHandlerDelegate)); + _ = ICELib.IceSetErrorHandler(Marshal.GetFunctionPointerForDelegate(s_iceErrorHandlerDelegate)); + _ = ICELib.IceSetIOErrorHandler(Marshal.GetFunctionPointerForDelegate(s_iceIoErrorHandlerDelegate)); + + _currentSmcConn = smcConn; + _currentIceConn = SMLib.SmcGetIceConnection(smcConn); + + Task.Run(() => + { + var token = _cancellationTokenSource.Token; + while (!token.IsCancellationRequested) HandleRequests(); + }, _cancellationTokenSource.Token); + } + + public void Dispose() + { + if (_currentSmcConn == IntPtr.Zero) return; + + s_nativeToManagedMapper.TryRemove(_currentSmcConn, out _); + + _ = SMLib.SmcCloseConnection(_currentSmcConn, 1, + new[] { $"{nameof(X11PlatformLifetimeEvents)} was disposed in managed code." }); + } + + private static void SmcSaveCompleteHandler(IntPtr smcConn, IntPtr clientData) + { + GetInstance(smcConn)?.SaveCompleteHandler(); + } + + private static X11PlatformLifetimeEvents? GetInstance(IntPtr smcConn) + { + return s_nativeToManagedMapper.TryGetValue(smcConn, out var instance) ? instance : null; + } + + private static void SmcShutdownCancelledHandler(IntPtr smcConn, IntPtr clientData) + { + GetInstance(smcConn)?.ShutdownCancelledHandler(); + } + + private static void SmcDieHandler(IntPtr smcConn, IntPtr clientData) + { + GetInstance(smcConn)?.DieHandler(); + } + + private static void SmcSaveYourselfHandler(IntPtr smcConn, IntPtr clientData, int saveType, + bool shutdown, int interactStyle, bool fast) + { + GetInstance(smcConn)?.SaveYourselfHandler(smcConn, clientData, shutdown, fast); + } + + private static void StaticInteractHandler(IntPtr smcConn, IntPtr clientData) + { + GetInstance(smcConn)?.InteractHandler(smcConn); + } + + private static void StaticIceIOErrorHandler(IntPtr iceConn) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(null, + "ICELib reported an unknown IO Error."); + } + + private static void StaticErrorHandler(IntPtr smcConn, bool swap, int offendingMinorOpcode, + ulong offendingSequence, int errorClass, int severity, IntPtr values) + { + GetInstance(smcConn) + ?.ErrorHandler(swap, offendingMinorOpcode, offendingSequence, errorClass, severity, values); + } + + // ReSharper disable UnusedParameter.Local + private void ErrorHandler(bool swap, int offendingMinorOpcode, ulong offendingSequence, int errorClass, + int severity, IntPtr values) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, + "SMLib reported an error:" + + $" severity {severity:X}" + + $" mOpcode {offendingMinorOpcode:X}" + + $" mSeq {offendingSequence:X}" + + $" errClass {errorClass:X}."); + } + + private void HandleRequests() + { + if (ICELib.IceProcessMessages(_currentIceConn, out _, out _) == + ICELib.IceProcessMessagesStatus.IceProcessMessagesIoError) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, + "SMLib lost its underlying ICE connection."); + Dispose(); + } + } + + private void SaveCompleteHandler() + { + _saveYourselfPhase = false; + } + + private void ShutdownCancelledHandler() + { + if (_saveYourselfPhase) + SMLib.SmcSaveYourselfDone(_currentSmcConn, true); + _saveYourselfPhase = false; + } + + private void DieHandler() + { + Dispose(); + } + + private void SaveYourselfHandler(IntPtr smcConn, IntPtr clientData, bool shutdown, bool fast) + { + if (_saveYourselfPhase) + { + SMLib.SmcSaveYourselfDone(smcConn, true); + } + + _saveYourselfPhase = true; + + if (shutdown && !fast) + { + var _ = SMLib.SmcInteractRequest(smcConn, SMLib.SmDialogValue.SmDialogError, + Marshal.GetFunctionPointerForDelegate(s_smcInteractDelegate), + clientData); + } + else + { + SMLib.SmcSaveYourselfDone(smcConn, true); + _saveYourselfPhase = false; + } + } + + private void InteractHandler(IntPtr smcConn) + { + Dispatcher.UIThread.Post(() => ActualInteractHandler(smcConn)); + } + + private void ActualInteractHandler(IntPtr smcConn) + { + var e = new ShutdownRequestedEventArgs(); + + if (_platform.Options?.EnableSessionManagement ?? false) + { + ShutdownRequested?.Invoke(this, e); + } + + SMLib.SmcInteractDone(smcConn, e.Cancel); + + if (e.Cancel) + { + return; + } + + _saveYourselfPhase = false; + + SMLib.SmcSaveYourselfDone(smcConn, true); + } + + private static void IceWatchHandler(IntPtr iceConn, IntPtr clientData, bool opening, IntPtr* watchData) + { + if (!opening) return; + + ICELib.IceRemoveConnectionWatch(Marshal.GetFunctionPointerForDelegate(s_iceWatchProcDelegate), + IntPtr.Zero); + } + + public event EventHandler? ShutdownRequested; + } +} diff --git a/src/Avalonia.X11/X11Window.Xim.cs b/src/Avalonia.X11/X11Window.Xim.cs index 444c82fd22..ecb23ff097 100644 --- a/src/Avalonia.X11/X11Window.Xim.cs +++ b/src/Avalonia.X11/X11Window.Xim.cs @@ -112,8 +112,8 @@ namespace Avalonia.X11 public ValueTask HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode) => new ValueTask(false); - public event Action Commit; - public event Action ForwardKey; + public event Action Commit { add { } remove { } } + public event Action ForwardKey { add { } remove { } } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 7bc8872fa7..b2bc9d1a35 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1026,6 +1026,7 @@ namespace Avalonia.X11 if (string.IsNullOrEmpty(title)) { XDeleteProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_NAME); + XDeleteProperty(_x11.Display, _handle, _x11.Atoms.XA_WM_NAME); } else { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 4ad68fc63e..db33b88cc3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.Converters; using Avalonia.Markup.Xaml.XamlIl.Runtime; +using Avalonia.Styling; namespace Avalonia.Markup.Xaml.MarkupExtensions { @@ -33,6 +34,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions _ => null, }; + if (provideTarget.TargetObject is Setter setter) + { + targetType = setter.Property.PropertyType; + } + // Look upwards though the ambient context for IResourceHosts and IResourceProviders // which might be able to give us the resource. foreach (var e in stack.Parents) diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index ac029f1062..49311ae1f0 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -5,6 +5,7 @@ Avalonia.Skia Avalonia.Skia true + true true diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 0ac42277e7..ec8a8436e1 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -25,7 +25,7 @@ namespace Avalonia.Skia GRGlInterface.CreateOpenGl(proc => context.GlInterface.GetProcAddress(proc)) : GRGlInterface.CreateGles(proc => context.GlInterface.GetProcAddress(proc))) { - _grContext = GRContext.CreateGl(iface); + _grContext = GRContext.CreateGl(iface, new GRContextOptions { AvoidStencilBuffers = true }); if (maxResourceBytes.HasValue) { _grContext.SetResourceCacheLimit(maxResourceBytes.Value); diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 21b2959089..2b9d0b103e 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -6,7 +6,7 @@ using SkiaSharp; namespace Avalonia.Skia { - internal class SKTypefaceCollection + public class SKTypefaceCollection { private readonly ConcurrentDictionary _typefaces = new ConcurrentDictionary(); diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index de77c9186e..2816146806 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -7,7 +7,7 @@ using SkiaSharp; namespace Avalonia.Skia { - internal static class SKTypefaceCollectionCache + public static class SKTypefaceCollectionCache { private static readonly ConcurrentDictionary s_cachedCollections; diff --git a/src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf b/src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf new file mode 100644 index 0000000000..3560a3a0c8 Binary files /dev/null and b/src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf differ diff --git a/src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf b/src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf new file mode 100644 index 0000000000..1639ad7d40 Binary files /dev/null and b/src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf differ diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj new file mode 100644 index 0000000000..b89bc556f7 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj @@ -0,0 +1,57 @@ + + + + net6.0 + enable + enable + Avalonia.Web.Blazor + preview + + + + + + + + + wwwroot + true + true + + + + false + true + + + true + false + + + + + + + + + + true + build\;buildTransitive\ + + + true + build\;buildTransitive\ + + + + + + + + + + + + + + diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets new file mode 100644 index 0000000000..e9052fda88 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs new file mode 100644 index 0000000000..65c73b101a --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Platform; + +namespace Avalonia.Web.Blazor +{ + public class AvaloniaBlazorAppBuilder : AppBuilderBase + { + public AvaloniaBlazorAppBuilder(IRuntimePlatform platform, Action platformServices) + : base(platform, platformServices) + { + } + + public AvaloniaBlazorAppBuilder() : base(BlazorRuntimePlatform.Instance, BlazorRuntimePlatform.RegisterServices) + { + UseWindowingSubsystem(BlazorWindowingPlatform.Register); + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor new file mode 100644 index 0000000000..584c77a62c --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor @@ -0,0 +1,19 @@ +
+ + + + +
diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs new file mode 100644 index 0000000000..ec3d665597 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -0,0 +1,375 @@ +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Embedding; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Avalonia.Web.Blazor.Interop; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + public partial class AvaloniaView : ITextInputMethodImpl + { + private readonly RazorViewTopLevelImpl _topLevelImpl; + private EmbeddableControlRoot _topLevel; + + // Interop + private SKHtmlCanvasInterop _interop = null!; + private SizeWatcherInterop _sizeWatcher = null!; + private DpiWatcherInterop _dpiWatcher = null!; + private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null!; + private InputHelperInterop _inputHelper = null!; + private ElementReference _htmlCanvas; + private ElementReference _inputElement; + private double _dpi; + private SKSize _canvasSize; + + private GRContext? _context; + private GRGlInterface? _glInterface; + private const SKColorType ColorType = SKColorType.Rgba8888; + + private bool _initialised; + + [Inject] private IJSRuntime Js { get; set; } = null!; + + public AvaloniaView() + { + _topLevelImpl = new RazorViewTopLevelImpl(this); + + _topLevel = new EmbeddableControlRoot(_topLevelImpl); + + if (Application.Current.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) + { + _topLevel.Content = lifetime.MainView; + } + } + + private void OnTouchStart(TouchEventArgs e) + { + foreach (var touch in e.ChangedTouches) + { + _topLevelImpl.RawTouchEvent(RawPointerEventType.TouchBegin, new Point(touch.ClientX, touch.ClientY), + GetModifiers(e), touch.Identifier); + } + } + + private void OnTouchEnd(TouchEventArgs e) + { + foreach (var touch in e.ChangedTouches) + { + _topLevelImpl.RawTouchEvent(RawPointerEventType.TouchEnd, new Point(touch.ClientX, touch.ClientY), + GetModifiers(e), touch.Identifier); + } + } + + private void OnTouchCancel(TouchEventArgs e) + { + foreach (var touch in e.ChangedTouches) + { + _topLevelImpl.RawTouchEvent(RawPointerEventType.TouchCancel, new Point(touch.ClientX, touch.ClientY), + GetModifiers(e), touch.Identifier); + } + } + + private void OnTouchMove(TouchEventArgs e) + { + foreach (var touch in e.ChangedTouches) + { + _topLevelImpl.RawTouchEvent(RawPointerEventType.TouchUpdate, new Point(touch.ClientX, touch.ClientY), + GetModifiers(e), touch.Identifier); + } + } + + private void OnMouseMove(MouseEventArgs e) + { + _topLevelImpl.RawMouseEvent(RawPointerEventType.Move, new Point(e.ClientX, e.ClientY), GetModifiers(e)); + } + + private void OnMouseUp(MouseEventArgs e) + { + RawPointerEventType type = default; + + switch (e.Button) + { + case 0: + type = RawPointerEventType.LeftButtonUp; + break; + + case 1: + type = RawPointerEventType.MiddleButtonUp; + break; + + case 2: + type = RawPointerEventType.RightButtonUp; + break; + } + + _topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e)); + } + + private void OnMouseDown(MouseEventArgs e) + { + RawPointerEventType type = default; + + switch (e.Button) + { + case 0: + type = RawPointerEventType.LeftButtonDown; + break; + + case 1: + type = RawPointerEventType.MiddleButtonDown; + break; + + case 2: + type = RawPointerEventType.RightButtonDown; + break; + } + + _topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e)); + } + + private void OnWheel(WheelEventArgs e) + { + _topLevelImpl.RawMouseWheelEvent(new Point(e.ClientX, e.ClientY), + new Vector(-(e.DeltaX / 50), -(e.DeltaY / 50)), GetModifiers(e)); + } + + private static RawInputModifiers GetModifiers(WheelEventArgs e) + { + var modifiers = RawInputModifiers.None; + + if (e.CtrlKey) + modifiers |= RawInputModifiers.Control; + if (e.AltKey) + modifiers |= RawInputModifiers.Alt; + if (e.ShiftKey) + modifiers |= RawInputModifiers.Shift; + if (e.MetaKey) + modifiers |= RawInputModifiers.Meta; + + if ((e.Buttons & 1L) == 1) + modifiers |= RawInputModifiers.LeftMouseButton; + + if ((e.Buttons & 2L) == 2) + modifiers |= RawInputModifiers.RightMouseButton; + + if ((e.Buttons & 4L) == 4) + modifiers |= RawInputModifiers.MiddleMouseButton; + + return modifiers; + } + + private static RawInputModifiers GetModifiers(TouchEventArgs e) + { + var modifiers = RawInputModifiers.None; + + if (e.CtrlKey) + modifiers |= RawInputModifiers.Control; + if (e.AltKey) + modifiers |= RawInputModifiers.Alt; + if (e.ShiftKey) + modifiers |= RawInputModifiers.Shift; + if (e.MetaKey) + modifiers |= RawInputModifiers.Meta; + + return modifiers; + } + + private static RawInputModifiers GetModifiers(MouseEventArgs e) + { + var modifiers = RawInputModifiers.None; + + if (e.CtrlKey) + modifiers |= RawInputModifiers.Control; + if (e.AltKey) + modifiers |= RawInputModifiers.Alt; + if (e.ShiftKey) + modifiers |= RawInputModifiers.Shift; + if (e.MetaKey) + modifiers |= RawInputModifiers.Meta; + + if ((e.Buttons & 1L) == 1) + modifiers |= RawInputModifiers.LeftMouseButton; + + if ((e.Buttons & 2L) == 2) + modifiers |= RawInputModifiers.RightMouseButton; + + if ((e.Buttons & 4L) == 4) + modifiers |= RawInputModifiers.MiddleMouseButton; + + return modifiers; + } + + private static RawInputModifiers GetModifiers(KeyboardEventArgs e) + { + var modifiers = RawInputModifiers.None; + + if (e.CtrlKey) + modifiers |= RawInputModifiers.Control; + if (e.AltKey) + modifiers |= RawInputModifiers.Alt; + if (e.ShiftKey) + modifiers |= RawInputModifiers.Shift; + if (e.MetaKey) + modifiers |= RawInputModifiers.Meta; + + return modifiers; + } + + private void OnKeyDown(KeyboardEventArgs e) + { + _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Key, GetModifiers(e)); + } + + private void OnKeyUp(KeyboardEventArgs e) + { + _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, GetModifiers(e)); + } + + private void OnInput(ChangeEventArgs e) + { + if (e.Value != null) + { + var inputData = e.Value.ToString(); + if (inputData != null) + { + _topLevelImpl.RawTextEvent(inputData); + } + } + + _inputHelper.Clear(); + } + + [Parameter(CaptureUnmatchedValues = true)] + public IReadOnlyDictionary? AdditionalAttributes { get; set; } + + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + { + Threading.Dispatcher.UIThread.Post(async () => + { + _inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement); + + _inputHelper.Hide(); + _inputHelper.SetCursor("default"); + + Console.WriteLine("starting html canvas setup"); + _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); + + Console.WriteLine("Interop created"); + _jsGlInfo = _interop.InitGL(); + + Console.WriteLine("jsglinfo created - init gl"); + + _sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged); + _dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged); + + Console.WriteLine("watchers created."); + + // create the SkiaSharp context + if (_context == null) + { + Console.WriteLine("create glcontext"); + _glInterface = GRGlInterface.Create(); + _context = GRContext.CreateGl(_glInterface); + + + // bump the default resource cache limit + _context.SetResourceCacheLimit(256 * 1024 * 1024); + Console.WriteLine("glcontext created and resource limit set"); + } + + _topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType, + new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); + + _initialised = true; + + _topLevel.Prepare(); + + _topLevel.Renderer.Start(); + Invalidate(); + }); + } + } + + private void OnRenderFrame() + { + if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null) + { + Console.WriteLine("nothing to render"); + return; + } + + ManualTriggerRenderTimer.Instance.RaiseTick(); + } + + public void Dispose() + { + _dpiWatcher.Unsubscribe(OnDpiChanged); + _sizeWatcher.Dispose(); + _interop.Dispose(); + } + + private void OnDpiChanged(double newDpi) + { + _dpi = newDpi; + + _topLevelImpl.SetClientSize(_canvasSize, _dpi); + + Invalidate(); + } + + private void OnSizeChanged(SKSize newSize) + { + _canvasSize = newSize; + + _topLevelImpl.SetClientSize(_canvasSize, _dpi); + + Invalidate(); + } + + public void Invalidate() + { + if (!_initialised || _canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null) + { + Console.WriteLine("invalidate ignored"); + return; + } + + _interop.RequestAnimationFrame(true, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + } + + public void SetActive(bool active) + { + _inputHelper.Clear(); + + if (active) + { + _inputHelper.Show(); + _inputHelper.Focus(); + } + else + { + _inputHelper.Hide(); + } + } + + public void SetCursorRect(Rect rect) + { + } + + public void SetOptions(TextInputOptionsQueryEventArgs options) + { + } + + public void Reset() + { + _inputHelper.Clear(); + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorRuntimePlatform.cs b/src/Web/Avalonia.Web.Blazor/BlazorRuntimePlatform.cs new file mode 100644 index 0000000000..9a5bf6b151 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/BlazorRuntimePlatform.cs @@ -0,0 +1,61 @@ +using System.Runtime.InteropServices; +using Avalonia.Platform; +using Avalonia.Shared.PlatformSupport; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorRuntimePlatform : IRuntimePlatform + { + public static readonly IRuntimePlatform Instance = new BlazorRuntimePlatform(); + + public IDisposable StartSystemTimer(TimeSpan interval, Action tick) + { + return new Timer(_ => tick(), null, interval, interval); + } + + public RuntimePlatformInfo GetRuntimeInfo() + { + return new RuntimePlatformInfo + { + IsDesktop = false, + IsMobile = false, + IsMono = true, + IsUnix = false, + IsCoreClr = false, + IsDotNetFramework = false + }; + } + + private class BasicBlob : IUnmanagedBlob + { + public BasicBlob(int size) + { + Address = Marshal.AllocHGlobal(size); + Size = size; + } + public void Dispose() + { + if (Address != IntPtr.Zero) + Marshal.FreeHGlobal(Address); + Address = IntPtr.Zero; + } + + public IntPtr Address { get; private set; } + + public int Size { get; } + public bool IsDisposed => Address == IntPtr.Zero; + } + + public IUnmanagedBlob AllocBlob(int size) + { + return new BasicBlob(size); + } + + public static void RegisterServices(AvaloniaBlazorAppBuilder builder) + { + AssetLoader.RegisterResUriParsers(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Instance); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new AssetLoader()); + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs b/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs new file mode 100644 index 0000000000..85ac57c746 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs @@ -0,0 +1,33 @@ +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Media; + +namespace Avalonia.Web.Blazor +{ + public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime + { + public Control? MainView { get; set; } + } + + public static class WebAppBuilder + { + public static T SetupWithSingleViewLifetime( + this T builder) + where T : AppBuilderBase, new() + { + return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); + } + + public static AvaloniaBlazorAppBuilder Configure() + where TApp : Application, new() + { + var builder = AvaloniaBlazorAppBuilder.Configure() + .UseSkia() + .With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManager(new CustomFontManagerImpl())); + + return builder; + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs new file mode 100644 index 0000000000..6fa7bf0bde --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs @@ -0,0 +1,25 @@ +using Avalonia.Skia; + +namespace Avalonia.Web.Blazor +{ + public class BlazorSkiaGpu : ISkiaGpu + { + public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces) + { + foreach (var surface in surfaces) + { + if (surface is BlazorSkiaSurface blazorSkiaSurface) + { + return new BlazorSkiaGpuRenderTarget(blazorSkiaSurface); + } + } + + return null; + } + + public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) + { + return null; + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs new file mode 100644 index 0000000000..0c53825131 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs @@ -0,0 +1,37 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession + { + private readonly SKSurface _surface; + + + public BlazorSkiaGpuRenderSession(BlazorSkiaSurface blazorSkiaSurface, GRBackendRenderTarget renderTarget) + { + _surface = SKSurface.Create(blazorSkiaSurface.Context, renderTarget, blazorSkiaSurface.Origin, blazorSkiaSurface.ColorType); + + GrContext = blazorSkiaSurface.Context; + + ScaleFactor = blazorSkiaSurface.Scaling; + + SurfaceOrigin = blazorSkiaSurface.Origin; + } + + public void Dispose() + { + _surface.Flush(); + + _surface.Dispose(); + } + + public GRContext GrContext { get; } + + public SKSurface SkSurface => _surface; + + public double ScaleFactor { get; } + + public GRSurfaceOrigin SurfaceOrigin { get; } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs new file mode 100644 index 0000000000..ee7374634f --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs @@ -0,0 +1,47 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget + { + private readonly GRBackendRenderTarget _renderTarget; + private readonly BlazorSkiaSurface _blazorSkiaSurface; + private readonly PixelSize _size; + + public BlazorSkiaGpuRenderTarget(BlazorSkiaSurface blazorSkiaSurface) + { + _size = blazorSkiaSurface.Size; + + var glFbInfo = new GRGlFramebufferInfo(blazorSkiaSurface.GlInfo.FboId, blazorSkiaSurface.ColorType.ToGlSizedFormat()); + { + _blazorSkiaSurface = blazorSkiaSurface; + _renderTarget = new GRBackendRenderTarget( + (int)(blazorSkiaSurface.Size.Width * blazorSkiaSurface.Scaling), + (int)(blazorSkiaSurface.Size.Height * blazorSkiaSurface.Scaling), + blazorSkiaSurface.GlInfo.Samples, + blazorSkiaSurface.GlInfo.Stencils, glFbInfo); + } + } + + public void Dispose() + { + _renderTarget.Dispose(); + } + + public ISkiaGpuRenderSession BeginRenderingSession() + { + return new BlazorSkiaGpuRenderSession(_blazorSkiaSurface, _renderTarget); + } + + public bool IsCorrupted + { + get + { + var result = _size.Width != _renderTarget.Width || _size.Height != _renderTarget.Height; + + return result; + } + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs new file mode 100644 index 0000000000..512309cfe3 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs @@ -0,0 +1,30 @@ +using Avalonia.Web.Blazor.Interop; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaSurface + { + public BlazorSkiaSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) + { + Context = context; + GlInfo = glInfo; + ColorType = colorType; + Size = size; + Scaling = scaling; + Origin = origin; + } + + public SKColorType ColorType { get; set; } + + public PixelSize Size { get; set; } + + public GRContext Context { get; set; } + + public GRSurfaceOrigin Origin { get; set; } + + public double Scaling { get; set; } + + public SKHtmlCanvasInterop.GLInfo GlInfo { get; set; } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs b/src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs new file mode 100644 index 0000000000..01837bbcd4 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs @@ -0,0 +1,78 @@ +using System.Globalization; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + public class CustomFontManagerImpl : IFontManagerImpl + { + private readonly Typeface[] _customTypefaces; + private readonly string _defaultFamilyName; + + private readonly Typeface _defaultTypeface = + new Typeface("avares://Avalonia.Web.Blazor/Assets#Noto Mono"); + private readonly Typeface _italicTypeface = + new Typeface("avares://Avalonia.Web.Blazor/Assets#Noto Sans"); + + public CustomFontManagerImpl() + { + _customTypefaces = new[] { _italicTypeface, _defaultTypeface }; + _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; + } + + public string GetDefaultFontFamilyName() + { + return _defaultFamilyName; + } + + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + return _customTypefaces.Select(x => x.FontFamily.Name); + } + + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, + CultureInfo culture, out Typeface typeface) + { + foreach (var customTypeface in _customTypefaces) + { + if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0) + { + continue; + } + + typeface = new Typeface(customTypeface.FontFamily, fontStyle, fontWeight); + + return true; + } + + typeface = _defaultTypeface; + + return true; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + SKTypeface skTypeface; + + switch (typeface.FontFamily.Name) + { + case "Noto Sans": + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } + default: + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); + skTypeface = typefaceCollection.Get(_defaultTypeface); + break; + } + } + + return new GlyphTypefaceImpl(skTypeface); + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs b/src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs new file mode 100644 index 0000000000..d6d504e07e --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel; +using Microsoft.JSInterop; + +namespace Avalonia.Web.Blazor.Interop +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public class ActionHelper + { + private readonly Action action; + + public ActionHelper(Action action) + { + this.action = action; + } + + [JSInvokable] + public void Invoke() => action?.Invoke(); + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs new file mode 100644 index 0000000000..29a2686b3f --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs @@ -0,0 +1,87 @@ +using System; +using System.Threading.Tasks; +using Microsoft.JSInterop; + +namespace Avalonia.Web.Blazor.Interop +{ + internal class DpiWatcherInterop : JSModuleInterop + { + private const string JsFilename = "./_content/Avalonia.Web.Blazor/DpiWatcher.js"; + private const string StartSymbol = "DpiWatcher.start"; + private const string StopSymbol = "DpiWatcher.stop"; + private const string GetDpiSymbol = "DpiWatcher.getDpi"; + + private static DpiWatcherInterop? instance; + + private event Action? callbacksEvent; + private readonly FloatFloatActionHelper callbackHelper; + + private DotNetObjectReference? callbackReference; + + public static async Task ImportAsync(IJSRuntime js, Action? callback = null) + { + var interop = Get(js); + await interop.ImportAsync(); + if (callback != null) + interop.Subscribe(callback); + return interop; + } + + public static DpiWatcherInterop Get(IJSRuntime js) => + instance ??= new DpiWatcherInterop(js); + + private DpiWatcherInterop(IJSRuntime js) + : base(js, JsFilename) + { + callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n)); + } + + protected override void OnDisposingModule() => + Stop(); + + public void Subscribe(Action callback) + { + var shouldStart = callbacksEvent == null; + + callbacksEvent += callback; + + var dpi = shouldStart + ? Start() + : GetDpi(); + + callback(dpi); + } + + public void Unsubscribe(Action callback) + { + callbacksEvent -= callback; + + if (callbacksEvent == null) + Stop(); + } + + private double Start() + { + if (callbackReference != null) + return GetDpi(); + + callbackReference = DotNetObjectReference.Create(callbackHelper); + + return Invoke(StartSymbol, callbackReference); + } + + private void Stop() + { + if (callbackReference == null) + return; + + Invoke(StopSymbol); + + callbackReference?.Dispose(); + callbackReference = null; + } + + public double GetDpi() => + Invoke(GetDpiSymbol); + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/FloatFloatActionHelper.cs b/src/Web/Avalonia.Web.Blazor/Interop/FloatFloatActionHelper.cs new file mode 100644 index 0000000000..80b9e5689a --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/FloatFloatActionHelper.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel; +using Microsoft.JSInterop; + +namespace Avalonia.Web.Blazor.Interop +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public class FloatFloatActionHelper + { + private readonly Action action; + + public FloatFloatActionHelper(Action action) + { + this.action = action; + } + + [JSInvokable] + public void Invoke(float width, float height) => action?.Invoke(width, height); + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs new file mode 100644 index 0000000000..6d71e0c48f --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using SkiaSharp; + +namespace Avalonia.Web.Blazor.Interop +{ + internal class InputHelperInterop : JSModuleInterop + { + private const string JsFilename = "./_content/Avalonia.Web.Blazor/InputHelper.js"; + private const string ClearSymbol = "InputHelper.clear"; + private const string FocusSymbol = "InputHelper.focus"; + private const string SetCursorSymbol = "InputHelper.setCursor"; + private const string HideSymbol = "InputHelper.hide"; + private const string ShowSymbol = "InputHelper.show"; + + private readonly ElementReference inputElement; + + public static async Task ImportAsync(IJSRuntime js, ElementReference element) + { + var interop = new InputHelperInterop(js, element); + await interop.ImportAsync(); + return interop; + } + + public InputHelperInterop(IJSRuntime js, ElementReference element) + : base(js, JsFilename) + { + inputElement = element; + } + + public void Clear() => Invoke(ClearSymbol, inputElement); + + public void Focus() => Invoke(FocusSymbol, inputElement); + + public void SetCursor(string kind) => Invoke(SetCursorSymbol, inputElement, kind); + + public void Hide() => Invoke(HideSymbol, inputElement); + + public void Show() => Invoke(ShowSymbol, inputElement); + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs new file mode 100644 index 0000000000..524326f7c0 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Microsoft.JSInterop; + +namespace Avalonia.Web.Blazor.Interop +{ + internal class JSModuleInterop : IDisposable + { + private readonly Task moduleTask; + private IJSUnmarshalledObjectReference? module; + + public JSModuleInterop(IJSRuntime js, string filename) + { + if (js is not IJSInProcessRuntime) + throw new NotSupportedException("SkiaSharp currently only works on Web Assembly."); + + moduleTask = js.InvokeAsync("import", filename).AsTask(); + } + + public async Task ImportAsync() + { + module = await moduleTask; + } + + public void Dispose() + { + OnDisposingModule(); + Module.Dispose(); + } + + protected IJSUnmarshalledObjectReference Module => + module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first."); + + protected void Invoke(string identifier, params object?[]? args) => + Module.InvokeVoid(identifier, args); + + protected TValue Invoke(string identifier, params object?[]? args) => + Module.Invoke(identifier, args); + + protected virtual void OnDisposingModule() { } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs new file mode 100644 index 0000000000..4f5d4cdf70 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using SkiaSharp; + +namespace Avalonia.Web.Blazor.Interop +{ + internal class SKHtmlCanvasInterop : JSModuleInterop + { + private const string JsFilename = "./_content/Avalonia.Web.Blazor/SKHtmlCanvas.js"; + private const string InitGLSymbol = "SKHtmlCanvas.initGL"; + private const string InitRasterSymbol = "SKHtmlCanvas.initRaster"; + private const string DeinitSymbol = "SKHtmlCanvas.deinit"; + private const string RequestAnimationFrameSymbol = "SKHtmlCanvas.requestAnimationFrame"; + private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData"; + + private readonly ElementReference htmlCanvas; + private readonly string htmlElementId; + private readonly ActionHelper callbackHelper; + + private DotNetObjectReference? callbackReference; + + public static async Task ImportAsync(IJSRuntime js, ElementReference element, Action callback) + { + var interop = new SKHtmlCanvasInterop(js, element, callback); + await interop.ImportAsync(); + return interop; + } + + public SKHtmlCanvasInterop(IJSRuntime js, ElementReference element, Action renderFrameCallback) + : base(js, JsFilename) + { + htmlCanvas = element; + htmlElementId = element.Id; + + callbackHelper = new ActionHelper(renderFrameCallback); + } + + protected override void OnDisposingModule() => + Deinit(); + + public GLInfo InitGL() + { + if (callbackReference != null) + throw new InvalidOperationException("Unable to initialize the same canvas more than once."); + + callbackReference = DotNetObjectReference.Create(callbackHelper); + + return Invoke(InitGLSymbol, htmlCanvas, htmlElementId, callbackReference); + } + + public bool InitRaster() + { + if (callbackReference != null) + throw new InvalidOperationException("Unable to initialize the same canvas more than once."); + + callbackReference = DotNetObjectReference.Create(callbackHelper); + + return Invoke(InitRasterSymbol, htmlCanvas, htmlElementId, callbackReference); + } + + public void Deinit() + { + if (callbackReference == null) + return; + + Invoke(DeinitSymbol, htmlElementId); + + callbackReference?.Dispose(); + } + + public void RequestAnimationFrame(bool enableRenderLoop, int rawWidth, int rawHeight) => + Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop, rawWidth, rawHeight); + + public void PutImageData(IntPtr intPtr, SKSizeI rawSize) => + Invoke(PutImageDataSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height); + + public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs new file mode 100644 index 0000000000..8904137b8b --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using SkiaSharp; + +namespace Avalonia.Web.Blazor.Interop +{ + internal class SizeWatcherInterop : JSModuleInterop + { + private const string JsFilename = "./_content/Avalonia.Web.Blazor/SizeWatcher.js"; + private const string ObserveSymbol = "SizeWatcher.observe"; + private const string UnobserveSymbol = "SizeWatcher.unobserve"; + + private readonly ElementReference htmlElement; + private readonly string htmlElementId; + private readonly FloatFloatActionHelper callbackHelper; + + private DotNetObjectReference? callbackReference; + + public static async Task ImportAsync(IJSRuntime js, ElementReference element, Action callback) + { + var interop = new SizeWatcherInterop(js, element, callback); + await interop.ImportAsync(); + interop.Start(); + return interop; + } + + public SizeWatcherInterop(IJSRuntime js, ElementReference element, Action callback) + : base(js, JsFilename) + { + htmlElement = element; + htmlElementId = element.Id; + callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y))); + } + + protected override void OnDisposingModule() => + Stop(); + + public void Start() + { + if (callbackReference != null) + return; + + callbackReference = DotNetObjectReference.Create(callbackHelper); + + Invoke(ObserveSymbol, htmlElement, htmlElementId, callbackReference); + } + + public void Stop() + { + if (callbackReference == null) + return; + + Invoke(UnobserveSymbol, htmlElementId); + + callbackReference?.Dispose(); + callbackReference = null; + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts new file mode 100644 index 0000000000..72baf14d8f --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts @@ -0,0 +1,41 @@ + +export class DpiWatcher { + static lastDpi: number; + static timerId: number; + static callback: DotNet.DotNetObjectReference; + + public static getDpi() { + return window.devicePixelRatio; + } + + public static start(callback: DotNet.DotNetObjectReference): number { + //console.info(`Starting DPI watcher with callback ${callback._id}...`); + + DpiWatcher.lastDpi = window.devicePixelRatio; + DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); + DpiWatcher.callback = callback; + + return DpiWatcher.lastDpi; + } + + public static stop() { + //console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`); + + window.clearInterval(DpiWatcher.timerId); + + DpiWatcher.callback = undefined; + } + + static update() { + if (!DpiWatcher.callback) + return; + + const currentDpi = window.devicePixelRatio; + const lastDpi = DpiWatcher.lastDpi; + DpiWatcher.lastDpi = currentDpi; + + if (Math.abs(lastDpi - currentDpi) > 0.001) { + DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi); + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts new file mode 100644 index 0000000000..a3bf9de31d --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts @@ -0,0 +1,23 @@ + +export class InputHelper { + public static clear (inputElement: HTMLInputElement){ + inputElement.value = ""; + } + + public static focus (inputElement: HTMLInputElement){ + inputElement.focus(); + inputElement.setSelectionRange(0, 0); + } + + public static setCursor (inputElement: HTMLInputElement, kind: string) { + inputElement.style.cursor = kind; + } + + public static hide (inputElement: HTMLInputElement){ + inputElement.style.display = 'none'; + } + + public static show (inputElement: HTMLInputElement){ + inputElement.style.display = 'block'; + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts new file mode 100644 index 0000000000..147e2a963f --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts @@ -0,0 +1,225 @@ +// aliases for emscripten +declare let GL: any; +declare let GLctx: WebGLRenderingContext; +declare let Module: EmscriptenModule; + +// container for gl info +type SKGLViewInfo = { + context: WebGLRenderingContext | WebGL2RenderingContext | undefined; + fboId: number; + stencil: number; + sample: number; + depth: number; +} + +// alias for a potential skia html canvas +type SKHtmlCanvasElement = { + SKHtmlCanvas: SKHtmlCanvas +} & HTMLCanvasElement + +export class SKHtmlCanvas { + static elements: Map; + + htmlCanvas: HTMLCanvasElement; + glInfo: SKGLViewInfo; + renderFrameCallback: DotNet.DotNetObjectReference; + renderLoopEnabled: boolean = false; + renderLoopRequest: number = 0; + + public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo { + var view = SKHtmlCanvas.init(true, element, elementId, callback); + if (!view || !view.glInfo) + return null; + + return view.glInfo; + } + + public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): boolean { + var view = SKHtmlCanvas.init(false, element, elementId, callback); + if (!view) + return false; + + return true; + } + + static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKHtmlCanvas { + var htmlCanvas = element as SKHtmlCanvasElement; + if (!htmlCanvas) { + console.error(`No canvas element was provided.`); + return null; + } + + if (!SKHtmlCanvas.elements) + SKHtmlCanvas.elements = new Map(); + SKHtmlCanvas.elements[elementId] = element; + + const view = new SKHtmlCanvas(useGL, element, callback); + + htmlCanvas.SKHtmlCanvas = view; + + return view; + } + + public static deinit(elementId: string) { + if (!elementId) + return; + + const element = SKHtmlCanvas.elements[elementId]; + SKHtmlCanvas.elements.delete(elementId); + + const htmlCanvas = element as SKHtmlCanvasElement; + if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) + return; + + htmlCanvas.SKHtmlCanvas.deinit(); + htmlCanvas.SKHtmlCanvas = undefined; + } + + public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean, width?: number, height?: number) { + const htmlCanvas = element as SKHtmlCanvasElement; + if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) + return; + + htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop, width, height); + } + + public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) { + const htmlCanvas = element as SKHtmlCanvasElement; + if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) + return; + + htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable); + } + + public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) { + const htmlCanvas = element as SKHtmlCanvasElement; + if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) + return; + + htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height); + } + + public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObjectReference) { + this.htmlCanvas = element; + this.renderFrameCallback = callback; + + if (useGL) { + const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas); + if (!ctx) { + console.error(`Failed to create WebGL context: err ${ctx}`); + return null; + } + + // make current + GL.makeContextCurrent(ctx); + + // read values + const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); + this.glInfo = { + context: ctx, + fboId: fbo ? fbo.id : 0, + stencil: GLctx.getParameter(GLctx.STENCIL_BITS), + sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES) + depth: GLctx.getParameter(GLctx.DEPTH_BITS), + }; + } + } + + public deinit() { + this.setEnableRenderLoop(false); + } + + public requestAnimationFrame(renderLoop?: boolean, width?: number, height?: number) { + // optionally update the render loop + if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) + this.setEnableRenderLoop(renderLoop); + + // make sure the canvas is scaled correctly for the drawing + if (width && height) { + this.htmlCanvas.width = width; + this.htmlCanvas.height = height; + } + + // skip because we have a render loop + if (this.renderLoopRequest !== 0) + return; + + // add the draw to the next frame + this.renderLoopRequest = window.requestAnimationFrame(() => { + if (this.glInfo) { + // make current + GL.makeContextCurrent(this.glInfo.context); + } + + this.renderFrameCallback.invokeMethod('Invoke'); + this.renderLoopRequest = 0; + + // we may want to draw the next frame + if (this.renderLoopEnabled) + this.requestAnimationFrame(); + }); + } + + public setEnableRenderLoop(enable: boolean) { + this.renderLoopEnabled = enable; + + // either start the new frame or cancel the existing one + if (enable) { + //console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`); + this.requestAnimationFrame(); + } else if (this.renderLoopRequest !== 0) { + window.cancelAnimationFrame(this.renderLoopRequest); + this.renderLoopRequest = 0; + } + } + + public putImageData(pData: number, width: number, height: number): boolean { + if (this.glInfo || !pData || width <= 0 || width <= 0) + return false; + + var ctx = this.htmlCanvas.getContext('2d'); + if (!ctx) { + console.error(`Failed to obtain 2D canvas context.`); + return false; + } + + // make sure the canvas is scaled correctly for the drawing + this.htmlCanvas.width = width; + this.htmlCanvas.height = height; + + // set the canvas to be the bytes + var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4); + var imageData = new ImageData(buffer, width, height); + ctx.putImageData(imageData, 0, 0); + + return true; + } + + static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { + const contextAttributes = { + alpha: 1, + depth: 1, + stencil: 8, + antialias: 0, + premultipliedAlpha: 1, + preserveDrawingBuffer: 0, + preferLowPowerToHighPerformance: 0, + failIfMajorPerformanceCaveat: 0, + majorVersion: 2, + minorVersion: 0, + enableExtensionsByDefault: 1, + explicitSwapControl: 0, + renderViaOffscreenBackBuffer: 1, + }; + + let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); + if (!ctx && contextAttributes.majorVersion > 1) { + console.warn('Falling back to WebGL 1.0'); + contextAttributes.majorVersion = 1; + contextAttributes.minorVersion = 0; + ctx = GL.createContext(htmlCanvas, contextAttributes); + } + + return ctx; + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts new file mode 100644 index 0000000000..88b94f3a80 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts @@ -0,0 +1,68 @@ + +type SizeWatcherElement = { + SizeWatcher: SizeWatcherInstance; +} & HTMLElement + +type SizeWatcherInstance = { + callback: DotNet.DotNetObjectReference; +} + +export class SizeWatcher { + static observer: ResizeObserver; + static elements: Map; + + public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObjectReference) { + if (!element || !callback) + return; + + //console.info(`Adding size watcher observation with callback ${callback._id}...`); + + SizeWatcher.init(); + + const watcherElement = element as SizeWatcherElement; + watcherElement.SizeWatcher = { + callback: callback + }; + + SizeWatcher.elements[elementId] = element; + SizeWatcher.observer.observe(element); + + SizeWatcher.invoke(element); + } + + public static unobserve(elementId: string) { + if (!elementId || !SizeWatcher.observer) + return; + + //console.info('Removing size watcher observation...'); + + const element = SizeWatcher.elements[elementId]; + + SizeWatcher.elements.delete(elementId); + SizeWatcher.observer.unobserve(element); + } + + static init() { + if (SizeWatcher.observer) + return; + + //console.info('Starting size watcher...'); + + SizeWatcher.elements = new Map(); + SizeWatcher.observer = new ResizeObserver((entries) => { + for (let entry of entries) { + SizeWatcher.invoke(entry.target); + } + }); + } + + static invoke(element: Element) { + const watcherElement = element as SizeWatcherElement; + const instance = watcherElement.SizeWatcher; + + if (!instance || !instance.callback) + return; + + return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight); + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts new file mode 100644 index 0000000000..4a3d71e034 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts @@ -0,0 +1,7 @@ + +declare namespace DotNet { + interface DotNetObjectReference extends DotNet.DotNetObject { + _id: number; + dispose(); + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts new file mode 100644 index 0000000000..ff6dc4a8f8 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts @@ -0,0 +1,56 @@ +// Type definitions for non-npm package @blazor/javascript-interop 3.1 +// Project: https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interop?view=aspnetcore-3.1 +// Definitions by: Piotr Błażejewicz (Peter Blazejewicz) +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// Minimum TypeScript Version: 3.0 + +// Here be dragons! +// This is community-maintained definition file intended to ease the process of developing +// high quality JavaScript interop code to be used in Blazor application from your C# .Net code. +// Could be removed without a notice in case official definition types ships with Blazor itself. + +// tslint:disable:no-unnecessary-generics + +declare namespace DotNet { + /** + * Invokes the specified .NET public method synchronously. Not all hosting scenarios support + * synchronous invocation, so if possible use invokeMethodAsync instead. + * + * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. + * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. + * @param args Arguments to pass to the method, each of which must be JSON-serializable. + * @returns The result of the operation. + */ + function invokeMethod(assemblyName: string, methodIdentifier: string, ...args: any[]): T; + /** + * Invokes the specified .NET public method asynchronously. + * + * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. + * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. + * @param args Arguments to pass to the method, each of which must be JSON-serializable. + * @returns A promise representing the result of the operation. + */ + function invokeMethodAsync(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise; + /** + * Represents the .NET instance passed by reference to JavaScript. + */ + interface DotNetObject { + /** + * Invokes the specified .NET instance public method synchronously. Not all hosting scenarios support + * synchronous invocation, so if possible use invokeMethodAsync instead. + * + * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. + * @param args Arguments to pass to the method, each of which must be JSON-serializable. + * @returns The result of the operation. + */ + invokeMethod(methodIdentifier: string, ...args: any[]): T; + /** + * Invokes the specified .NET instance public method asynchronously. + * + * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. + * @param args Arguments to pass to the method, each of which must be JSON-serializable. + * @returns A promise representing the result of the operation. + */ + invokeMethodAsync(methodIdentifier: string, ...args: any[]): Promise; + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts new file mode 100644 index 0000000000..e3829d4db1 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts @@ -0,0 +1,326 @@ +// Type definitions for Emscripten 1.39.16 +// Project: https://emscripten.org +// Definitions by: Kensuke Matsuzaki +// Periklis Tsirakidis +// Bumsik Kim +// Louis DeScioli +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.2 + +/** Other WebAssembly declarations, for compatibility with older versions of Typescript */ +declare namespace WebAssembly { + interface Module {} +} + +declare namespace Emscripten { + interface FileSystemType {} + type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER'; + + type JSType = 'number' | 'string' | 'array' | 'boolean'; + type TypeCompatibleWithC = number | string | any[] | boolean; + + type CIntType = 'i8' | 'i16' | 'i32' | 'i64'; + type CFloatType = 'float' | 'double'; + type CPointerType = 'i8*' | 'i16*' | 'i32*' | 'i64*' | 'float*' | 'double*' | '*'; + type CType = CIntType | CFloatType | CPointerType; + + type WebAssemblyImports = Array<{ + name: string; + kind: string; + }>; + + type WebAssemblyExports = Array<{ + module: string; + name: string; + kind: string; + }>; + + interface CCallOpts { + async?: boolean | undefined; + } +} + +interface EmscriptenModule { + print(str: string): void; + printErr(str: string): void; + arguments: string[]; + environment: Emscripten.EnvironmentType; + preInit: Array<{ (): void }>; + preRun: Array<{ (): void }>; + postRun: Array<{ (): void }>; + onAbort: { (what: any): void }; + onRuntimeInitialized: { (): void }; + preinitializedWebGLContext: WebGLRenderingContext; + noInitialRun: boolean; + noExitRuntime: boolean; + logReadFiles: boolean; + filePackagePrefixURL: string; + wasmBinary: ArrayBuffer; + + destroy(object: object): void; + getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer; + instantiateWasm( + imports: Emscripten.WebAssemblyImports, + successCallback: (module: WebAssembly.Module) => void, + ): Emscripten.WebAssemblyExports; + locateFile(url: string, scriptDirectory: string): string; + onCustomMessage(event: MessageEvent): void; + + // USE_TYPED_ARRAYS == 1 + HEAP: Int32Array; + IHEAP: Int32Array; + FHEAP: Float64Array; + + // USE_TYPED_ARRAYS == 2 + HEAP8: Int8Array; + HEAP16: Int16Array; + HEAP32: Int32Array; + HEAPU8: Uint8Array; + HEAPU16: Uint16Array; + HEAPU32: Uint32Array; + HEAPF32: Float32Array; + HEAPF64: Float64Array; + + TOTAL_STACK: number; + TOTAL_MEMORY: number; + FAST_MEMORY: number; + + addOnPreRun(cb: () => any): void; + addOnInit(cb: () => any): void; + addOnPreMain(cb: () => any): void; + addOnExit(cb: () => any): void; + addOnPostRun(cb: () => any): void; + + preloadedImages: any; + preloadedAudios: any; + + _malloc(size: number): number; + _free(ptr: number): void; +} + +/** + * A factory function is generated when setting the `MODULARIZE` build option + * to `1` in your Emscripten build. It return a Promise that resolves to an + * initialized, ready-to-call `EmscriptenModule` instance. + * + * By default, the factory function will be named `Module`. It's recommended to + * use the `EXPORT_ES6` option, in which the factory function will be the + * default export. If used without `EXPORT_ES6`, the factory function will be a + * global variable. You can rename the variable using the `EXPORT_NAME` build + * option. It's left to you to declare any global variables as needed in your + * application's types. + * @param moduleOverrides Default properties for the initialized module. + */ +type EmscriptenModuleFactory = ( + moduleOverrides?: Partial, +) => Promise; + +declare namespace FS { + interface Lookup { + path: string; + node: FSNode; + } + + interface FSStream {} + interface FSNode {} + interface ErrnoError {} + + let ignorePermissions: boolean; + let trackingDelegate: any; + let tracking: any; + let genericErrors: any; + + // + // paths + // + function lookupPath(path: string, opts: any): Lookup; + function getPath(node: FSNode): string; + + // + // nodes + // + function isFile(mode: number): boolean; + function isDir(mode: number): boolean; + function isLink(mode: number): boolean; + function isChrdev(mode: number): boolean; + function isBlkdev(mode: number): boolean; + function isFIFO(mode: number): boolean; + function isSocket(mode: number): boolean; + + // + // devices + // + function major(dev: number): number; + function minor(dev: number): number; + function makedev(ma: number, mi: number): number; + function registerDevice(dev: number, ops: any): void; + + // + // core + // + function syncfs(populate: boolean, callback: (e: any) => any): void; + function syncfs(callback: (e: any) => any, populate?: boolean): void; + function mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any; + function unmount(mountpoint: string): void; + + function mkdir(path: string, mode?: number): any; + function mkdev(path: string, mode?: number, dev?: number): any; + function symlink(oldpath: string, newpath: string): any; + function rename(old_path: string, new_path: string): void; + function rmdir(path: string): void; + function readdir(path: string): any; + function unlink(path: string): void; + function readlink(path: string): string; + function stat(path: string, dontFollow?: boolean): any; + function lstat(path: string): any; + function chmod(path: string, mode: number, dontFollow?: boolean): void; + function lchmod(path: string, mode: number): void; + function fchmod(fd: number, mode: number): void; + function chown(path: string, uid: number, gid: number, dontFollow?: boolean): void; + function lchown(path: string, uid: number, gid: number): void; + function fchown(fd: number, uid: number, gid: number): void; + function truncate(path: string, len: number): void; + function ftruncate(fd: number, len: number): void; + function utime(path: string, atime: number, mtime: number): void; + function open(path: string, flags: string, mode?: number, fd_start?: number, fd_end?: number): FSStream; + function close(stream: FSStream): void; + function llseek(stream: FSStream, offset: number, whence: number): any; + function read(stream: FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number; + function write( + stream: FSStream, + buffer: ArrayBufferView, + offset: number, + length: number, + position?: number, + canOwn?: boolean, + ): number; + function allocate(stream: FSStream, offset: number, length: number): void; + function mmap( + stream: FSStream, + buffer: ArrayBufferView, + offset: number, + length: number, + position: number, + prot: number, + flags: number, + ): any; + function ioctl(stream: FSStream, cmd: any, arg: any): any; + function readFile(path: string, opts: { encoding: 'binary'; flags?: string | undefined }): Uint8Array; + function readFile(path: string, opts: { encoding: 'utf8'; flags?: string | undefined }): string; + function readFile(path: string, opts?: { flags?: string | undefined }): Uint8Array; + function writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string | undefined }): void; + + // + // module-level FS code + // + function cwd(): string; + function chdir(path: string): void; + function init( + input: null | (() => number | null), + output: null | ((c: number) => any), + error: null | ((c: number) => any), + ): void; + + function createLazyFile( + parent: string | FSNode, + name: string, + url: string, + canRead: boolean, + canWrite: boolean, + ): FSNode; + function createPreloadedFile( + parent: string | FSNode, + name: string, + url: string, + canRead: boolean, + canWrite: boolean, + onload?: () => void, + onerror?: () => void, + dontCreateFile?: boolean, + canOwn?: boolean, + ): void; + function createDataFile( + parent: string | FSNode, + name: string, + data: ArrayBufferView, + canRead: boolean, + canWrite: boolean, + canOwn: boolean, + ): FSNode; +} + +declare var MEMFS: Emscripten.FileSystemType; +declare var NODEFS: Emscripten.FileSystemType; +declare var IDBFS: Emscripten.FileSystemType; + +// Below runtime function/variable declarations are exportable by +// -s EXTRA_EXPORTED_RUNTIME_METHODS. You can extend or merge +// EmscriptenModule interface to add runtime functions. +// +// For example, by using -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']" +// You can access ccall() via Module["ccall"]. In this case, you should +// extend EmscriptenModule to pass the compiler check like the following: +// +// interface YourOwnEmscriptenModule extends EmscriptenModule { +// ccall: typeof ccall; +// } +// +// See: https://emscripten.org/docs/getting_started/FAQ.html#why-do-i-get-typeerror-module-something-is-not-a-function + +declare function ccall( + ident: string, + returnType: Emscripten.JSType | null, + argTypes: Emscripten.JSType[], + args: Emscripten.TypeCompatibleWithC[], + opts?: Emscripten.CCallOpts, +): any; +declare function cwrap( + ident: string, + returnType: Emscripten.JSType | null, + argTypes: Emscripten.JSType[], + opts?: Emscripten.CCallOpts, +): (...args: any[]) => any; + +declare function setValue(ptr: number, value: any, type: Emscripten.CType, noSafe?: boolean): void; +declare function getValue(ptr: number, type: Emscripten.CType, noSafe?: boolean): number; + +declare function allocate( + slab: number[] | ArrayBufferView | number, + types: Emscripten.CType | Emscripten.CType[], + allocator: number, + ptr?: number, +): number; + +declare function stackAlloc(size: number): number; +declare function stackSave(): number; +declare function stackRestore(ptr: number): void; + +declare function UTF8ToString(ptr: number, maxBytesToRead?: number): string; +declare function stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void; +declare function lengthBytesUTF8(str: string): number; +declare function allocateUTF8(str: string): number; +declare function allocateUTF8OnStack(str: string): number; +declare function UTF16ToString(ptr: number): string; +declare function stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void; +declare function lengthBytesUTF16(str: string): number; +declare function UTF32ToString(ptr: number): string; +declare function stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void; +declare function lengthBytesUTF32(str: string): number; + +declare function intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[]; +declare function intArrayToString(array: number[]): string; +declare function writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void; +declare function writeArrayToMemory(array: number[], buffer: number): void; +declare function writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void; + +declare function addRunDependency(id: any): void; +declare function removeRunDependency(id: any): void; + +declare function addFunction(func: (...args: any[]) => any, signature?: string): number; +declare function removeFunction(funcPtr: number): void; + +declare var ALLOC_NORMAL: number; +declare var ALLOC_STACK: number; +declare var ALLOC_STATIC: number; +declare var ALLOC_DYNAMIC: number; +declare var ALLOC_NONE: number; diff --git a/src/Web/Avalonia.Web.Blazor/Keycodes.cs b/src/Web/Avalonia.Web.Blazor/Keycodes.cs new file mode 100644 index 0000000000..ea30f0a9f0 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Keycodes.cs @@ -0,0 +1,127 @@ +using Avalonia.Input; + +namespace Avalonia.Web.Blazor +{ + internal static class Keycodes + { + public static Dictionary 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 } + }; + } +} diff --git a/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs b/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs new file mode 100644 index 0000000000..1a06d47744 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs @@ -0,0 +1,16 @@ +using System.Diagnostics; +using Avalonia.Rendering; + +namespace Avalonia.Web.Blazor +{ + public class ManualTriggerRenderTimer : IRenderTimer + { + private static readonly Stopwatch s_sw = Stopwatch.StartNew(); + + public static ManualTriggerRenderTimer Instance { get; } = new(); + + public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed); + + public event Action? Tick; + } +} diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs new file mode 100644 index 0000000000..8cd4d7b111 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -0,0 +1,165 @@ +using System.Diagnostics; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Web.Blazor.Interop; +using SkiaSharp; + +#nullable enable + +namespace Avalonia.Web.Blazor +{ + internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod + { + private Size _clientSize; + private BlazorSkiaSurface? _currentSurface; + private IInputRoot? _inputRoot; + private readonly Stopwatch _sw = Stopwatch.StartNew(); + private readonly ITextInputMethodImpl _textInputMethod; + private readonly TouchDevice _touchDevice; + + public RazorViewTopLevelImpl(ITextInputMethodImpl textInputMethod) + { + _textInputMethod = textInputMethod; + TransparencyLevel = WindowTransparencyLevel.None; + AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); + _touchDevice = new TouchDevice(); + } + + public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; + + + internal void SetSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling) + { + _currentSurface = + new BlazorSkiaSurface(context, glInfo, colorType, size, scaling, GRSurfaceOrigin.BottomLeft); + } + + public void SetClientSize(SKSize size, double dpi) + { + var newSize = new Size(size.Width, size.Height); + + if (newSize != _clientSize) + { + _clientSize = newSize; + + if (_currentSurface is { }) + { + _currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); + } + + Resized?.Invoke(newSize, PlatformResizeReason.User); + } + } + + public void RawTouchEvent(RawPointerEventType type, Point p, RawInputModifiers modifiers, long touchPointId) + { + if (_inputRoot is { }) + { + Input?.Invoke(new RawTouchEventArgs(_touchDevice, Timestamp, _inputRoot, type, p, modifiers, touchPointId)); + } + } + + public void RawMouseEvent(RawPointerEventType type, Point p, RawInputModifiers modifiers) + { + if (_inputRoot is { }) + { + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, _inputRoot, type, p, modifiers)); + } + } + + public void RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) + { + if (_inputRoot is { }) + { + Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers)); + } + } + + public void RawKeyboardEvent(RawKeyEventType type, string key, RawInputModifiers modifiers) + { + if (Keycodes.KeyCodes.TryGetValue(key, out var avkey)) + { + if (_inputRoot is { }) + { + Input?.Invoke(new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers)); + } + } + } + + public void RawTextEvent(string text) + { + if (_inputRoot is { }) + { + Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text)); + } + } + + public void Dispose() + { + + } + + public IRenderer CreateRenderer(IRenderRoot root) + { + var loop = AvaloniaLocator.Current.GetService(); + + return new DeferredRenderer(root, loop); + } + + 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) + { + // nop + + } + + public IPopupImpl? CreatePopup() + { + return null; + } + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) + { + + } + + public Size ClientSize => _clientSize; + public Size? FrameSize => null; + public double RenderScaling => 1; + + public IEnumerable Surfaces => new object[] { _currentSurface! }; + + public Action? Input { get; set; } + public Action? Paint { get; set; } + public Action? Resized { get; set; } + public Action? ScalingChanged { get; set; } + public Action? TransparencyLevelChanged { get; set; } + public Action? Closed { get; set; } + public Action? LostFocus { get; set; } + public IMouseDevice MouseDevice { get; } = new MouseDevice(); + + public IKeyboardDevice KeyboardDevice { get; } = BlazorWindowingPlatform.Keyboard; + public WindowTransparencyLevel TransparencyLevel { get; } + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } + + public ITextInputMethodImpl TextInputMethod => _textInputMethod; + } +} diff --git a/src/Web/Avalonia.Web.Blazor/WinStubs.cs b/src/Web/Avalonia.Web.Blazor/WinStubs.cs new file mode 100644 index 0000000000..13d83a9ee3 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/WinStubs.cs @@ -0,0 +1,80 @@ +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Web.Blazor +{ + internal class ClipboardStub : IClipboard + { + public Task GetTextAsync() => Task.FromResult(""); + + public Task SetTextAsync(string text) => Task.CompletedTask; + + public Task ClearAsync() => Task.CompletedTask; + + public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + + public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); + + public Task GetDataAsync(string format) => Task.FromResult(new ()); + } + + internal class CursorStub : ICursorImpl + { + public void Dispose() + { + + } + } + + internal class CursorFactoryStub : ICursorFactory + { + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + return new CursorStub(); + } + + ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) + { + return new CursorStub(); + } + } + + 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 SystemDialogsStub : ISystemDialogImpl + { + public Task ShowFileDialogAsync(FileDialog dialog, Window parent) => + Task.FromResult((string[]?)null); + + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) => + Task.FromResult((string?)null); + } + + internal class ScreenStub : IScreenImpl + { + public int ScreenCount => 1; + + public IReadOnlyList AllScreens { get; } = + new[] { new Screen(96, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) }; + } +} diff --git a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs b/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs new file mode 100644 index 0000000000..a3f2de5b61 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs @@ -0,0 +1,100 @@ +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; + +#nullable enable + +namespace Avalonia.Web.Blazor +{ + public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface + { + private bool _signaled; + private static int s_uiThreadId = -1; + + public IWindowImpl CreateWindow() => throw new NotSupportedException(); + + IWindowImpl IWindowingPlatform.CreateEmbeddableWindow() + { + throw new NotImplementedException(); + } + + public ITrayIconImpl? CreateTrayIcon() + { + return null; + } + + public static KeyboardDevice Keyboard { get; private set; } + + public static void Register() + { + var instance = new BlazorWindowingPlatform(); + Keyboard = new KeyboardDevice(); + AvaloniaLocator.CurrentMutable + .Bind().ToSingleton() + .Bind().ToSingleton() + .Bind().ToConstant(Keyboard) + .Bind().ToConstant(instance) + .Bind().ToConstant(instance) + .Bind().ToConstant(new RenderLoop()) + .Bind().ToConstant(ManualTriggerRenderTimer.Instance) + .Bind().ToSingleton() + .Bind().ToConstant(instance) + .Bind().ToSingleton() + .Bind().ToSingleton(); + } + + public Size DoubleClickSize { get; } = new Size(2, 2); + + public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); + + public void RunLoop(CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) + { + return AvaloniaLocator.Current.GetService() + .StartSystemTimer(interval, () => + { + Dispatcher.UIThread.RunJobs(priority); + tick(); + }); + } + + public void Signal(DispatcherPriority priority) + { + if (_signaled) + return; + + _signaled = true; + + IDisposable? disp = null; + + disp = AvaloniaLocator.Current.GetService() + .StartSystemTimer(TimeSpan.FromMilliseconds(1), + () => + { + _signaled = false; + disp?.Dispose(); + + Signaled?.Invoke(null); + }); + } + + public bool CurrentThreadIsLoopThread + { + get + { + return true; // Blazor is single threaded. + } + } + + public event Action? Signaled; + + + } +} diff --git a/src/Web/Avalonia.Web.Blazor/_Imports.razor b/src/Web/Avalonia.Web.Blazor/_Imports.razor new file mode 100644 index 0000000000..77285129da --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/_Imports.razor @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Components.Web diff --git a/src/Web/Avalonia.Web.Blazor/tsconfig.json b/src/Web/Avalonia.Web.Blazor/tsconfig.json new file mode 100644 index 0000000000..71c462cd60 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "noImplicitAny": false, + "noEmitOnError": true, + "removeComments": false, + "sourceMap": true, + "target": "ES2020", + "module": "ES2020", + "outDir": "wwwroot" + }, + "exclude": [ + "node_modules" + ] +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index f7ed2f215f..eaf6b47f42 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -193,7 +193,6 @@ namespace Avalonia.Win32 case WindowsMessage.WM_MBUTTONUP: case WindowsMessage.WM_XBUTTONUP: { - shouldTakeFocus = ShouldTakeFocusOnClick; if (ShouldIgnoreTouchEmulatedMessage()) { break; diff --git a/src/tools/MicroComGenerator/CSharpGen.Utils.cs b/src/tools/MicroComGenerator/CSharpGen.Utils.cs index da845b0ecd..28baaa65f8 100644 --- a/src/tools/MicroComGenerator/CSharpGen.Utils.cs +++ b/src/tools/MicroComGenerator/CSharpGen.Utils.cs @@ -40,7 +40,7 @@ namespace MicroComGenerator SyntaxToken Semicolon() => Token(SyntaxKind.SemicolonToken); static VariableDeclarationSyntax DeclareVar(string type, string name, - ExpressionSyntax? initializer = null) + ExpressionSyntax initializer = null) => VariableDeclaration(ParseTypeName(type), SingletonSeparatedList(VariableDeclarator(name) .WithInitializer(initializer == null ? null : EqualsValueClause(initializer)))); diff --git a/src/tools/MicroComGenerator/MicroComGenerator.csproj b/src/tools/MicroComGenerator/MicroComGenerator.csproj index 5ae431b4b9..68895b96ca 100644 --- a/src/tools/MicroComGenerator/MicroComGenerator.csproj +++ b/src/tools/MicroComGenerator/MicroComGenerator.csproj @@ -1,8 +1,8 @@ - - Exe - netcoreapp3.1 - + + Exe + net6.0 + diff --git a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs index b01fb70f58..26d8059eec 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimatableTests.cs @@ -1,5 +1,7 @@ using System; +using Avalonia.Animation.Animators; using Avalonia.Controls; +using Avalonia.Controls.Shapes; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Media; @@ -100,6 +102,71 @@ namespace Avalonia.Animation.UnitTests Times.Never); } + + [Theory] + [InlineData(null)] //null value + [InlineData("stringValue")] //string value + public void Invalid_Values_In_Animation_Should_Not_Crash_Animations(object invalidValue) + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Layoutable.WidthProperty, 1d), + }, + KeyTime = TimeSpan.FromSeconds(0) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Layoutable.WidthProperty, 2d), + }, + KeyTime = TimeSpan.FromSeconds(2), + }; + + var keyframe3 = new KeyFrame() + { + Setters = + { + new Setter(Layoutable.WidthProperty, invalidValue), + }, + KeyTime = TimeSpan.FromSeconds(3), + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(3), + Children = + { + keyframe1, + keyframe2, + keyframe3 + }, + IterationCount = new IterationCount(5), + PlaybackDirection = PlaybackDirection.Alternate, + }; + + var rect = new Rectangle() + { + Width = 11, + }; + + var originalValue = rect.Width; + + var clock = new TestClock(); + var animationRun = animation.RunAsync(rect, clock); + + clock.Step(TimeSpan.Zero); + Assert.Equal(rect.Width, 1); + clock.Step(TimeSpan.FromSeconds(2)); + Assert.Equal(rect.Width, 2); + clock.Step(TimeSpan.FromSeconds(3)); + //here we have invalid value so value should be expected and set to initial original value + Assert.Equal(rect.Width, originalValue); + } + [Fact] public void Transition_Is_Not_Applied_When_StyleTrigger_Changes_With_LocalValue_Present() { diff --git a/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj b/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj index 5b686dea4c..e07ece5460 100644 --- a/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj +++ b/tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net47 + net6.0 Library true diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index c4c1f49346..9628d16a6c 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net47 + net6.0 Library true latest diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index e8e69efdbc..3994d20c68 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp3.1 + net6.0 Exe false diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj index c0c9303767..eb1bf24d0c 100644 --- a/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj +++ b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj @@ -1,6 +1,6 @@ - netcoreapp3.1;net47 + net6.0 latest Library true diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index 6b17427eda..8dd8e843ac 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net47 + net6.0 latest Library true diff --git a/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs index 529b3b1aa8..22a9b28648 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls.UnitTests private class InvalidCollection : INotifyCollectionChanged, IEnumerable { - public event NotifyCollectionChangedEventHandler CollectionChanged; + public event NotifyCollectionChangedEventHandler CollectionChanged { add { } remove { } } public IEnumerator GetEnumerator() { diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index 1a11091b81..9eedd17716 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -540,6 +541,22 @@ namespace Avalonia.Controls.UnitTests.Platform Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true), Times.Never); Assert.True(e.Handled); } + + [Fact] + public void PointerPressed_On_Disabled_Item_Doesnt_Close_SubMenu() + { + var target = new DefaultMenuInteractionHandler(false); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.IsSubMenuOpen == true && x.Parent == menu); + var popup = new Popup(); + var e = CreatePressed(popup); + + ((ISetLogicalParent)popup).SetParent(parentItem); + target.PointerPressed(parentItem, e); + + Mock.Get(parentItem).Verify(x => x.Close(), Times.Never); + Assert.True(e.Handled); + } } public class ContextMenu diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 090a20c857..24e4631aff 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -5,6 +5,7 @@ using System.Linq; using Moq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Templates; using Avalonia.Layout; using Avalonia.LogicalTree; @@ -595,6 +596,113 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Popup_Should_Follow_Placement_Target_On_Window_Move() + { + using (CreateServices()) + { + var popup = new Popup { Width = 400, Height = 200 }; + var window = PreparedWindow(popup); + popup.Open(); + + if (popup.Host is PopupRoot popupRoot) + { + // Moving the window must move the popup (screen coordinates have changed) + var raised = false; + popupRoot.PositionChanged += (_, args) => + { + Assert.Equal(new PixelPoint(10, 10), args.Point); + raised = true; + }; + + window.Position = new PixelPoint(10, 10); + Assert.True(raised); + } + } + } + + [Fact] + public void Popup_Should_Follow_Placement_Target_On_Window_Resize() + { + using (CreateServices()) + { + + var placementTarget = new Panel() + { + Width = 10, + Height = 10, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + var popup = new Popup() { PlacementTarget = placementTarget, Width = 10, Height = 10 }; + ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); + + var window = PreparedWindow(placementTarget); + window.Show(); + popup.Open(); + + // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window + Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + + if (popup.Host is PopupRoot popupRoot) + { + // Resizing the window to 700x500 must move the popup to (345,255) as this is the new + // location of the placement target + var raised = false; + popupRoot.PositionChanged += (_, args) => + { + Assert.Equal(new PixelPoint(345, 255), args.Point); + raised = true; + }; + + window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + Assert.True(raised); + } + } + } + + [Fact] + public void Popup_Should_Follow_Popup_Root_Placement_Target() + { + // When the placement target of a popup is another popup (e.g. nested menu items), the child popup must + // follow the parent popup if it moves (due to root window movement or resize) + using (CreateServices()) + { + // The child popup is placed directly over the parent popup for position testing + var parentPopup = new Popup() { Width = 10, Height = 10 }; + var childPopup = new Popup() { + Width = 20, + Height = 20, + PlacementTarget = parentPopup, + PlacementMode = PlacementMode.AnchorAndGravity, + PlacementAnchor = PopupAnchor.TopLeft, + PlacementGravity = PopupGravity.BottomRight + }; + ((ISetLogicalParent)childPopup).SetParent(childPopup.PlacementTarget); + + var window = PreparedWindow(parentPopup); + window.Show(); + parentPopup.Open(); + childPopup.Open(); + + if (childPopup.Host is PopupRoot popupRoot) + { + var raised = false; + popupRoot.PositionChanged += (_, args) => + { + // The parent's initial placement is (395,295) which is a 10x10 popup centered + // in a 800x600 window. When the window is moved, the child's final placement is (405, 305) + // which is the parent's placement moved 10 pixels left and down. + Assert.Equal(new PixelPoint(405, 305), args.Point); + raised = true; + }; + + window.Position = new PixelPoint(10, 10); + Assert.True(raised); + } + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: diff --git a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj index 523c5e7ff4..5358b71571 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj +++ b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp3.1 + net6.0 false diff --git a/tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj b/tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj index dad500e3c6..3af8d6e0d7 100644 --- a/tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj +++ b/tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1 + net6.0 Library @@ -18,4 +18,4 @@ - \ No newline at end of file + diff --git a/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs b/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs index d76ed9042f..6e32158fcb 100644 --- a/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs +++ b/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs @@ -29,25 +29,25 @@ namespace Avalonia.DesignerSupport.Tests [SkippableTheory, InlineData( - @"..\..\..\..\..\tests/Avalonia.DesignerSupport.TestApp/bin/$BUILD/netcoreapp3.1/", + @"..\..\..\..\..\tests/Avalonia.DesignerSupport.TestApp/bin/$BUILD/net6.0/", "Avalonia.DesignerSupport.TestApp", "Avalonia.DesignerSupport.TestApp.dll", @"..\..\..\..\..\tests\Avalonia.DesignerSupport.TestApp\MainWindow.xaml", "win32"), InlineData( - @"..\..\..\..\..\samples\ControlCatalog.NetCore\bin\$BUILD\netcoreapp3.1\", + @"..\..\..\..\..\samples\ControlCatalog.NetCore\bin\$BUILD\net6.0\", "ControlCatalog.NetCore", "ControlCatalog.dll", @"..\..\..\..\..\samples\ControlCatalog\MainWindow.xaml", "win32"), InlineData( - @"..\..\..\..\..\tests/Avalonia.DesignerSupport.TestApp/bin/$BUILD/netcoreapp3.1/", + @"..\..\..\..\..\tests/Avalonia.DesignerSupport.TestApp/bin/$BUILD/net6.0/", "Avalonia.DesignerSupport.TestApp", "Avalonia.DesignerSupport.TestApp.dll", @"..\..\..\..\..\tests\Avalonia.DesignerSupport.TestApp\MainWindow.xaml", "avalonia-remote"), InlineData( - @"..\..\..\..\..\samples\ControlCatalog.NetCore\bin\$BUILD\netcoreapp3.1\", + @"..\..\..\..\..\samples\ControlCatalog.NetCore\bin\$BUILD\net6.0\", "ControlCatalog.NetCore", "ControlCatalog.dll", @"..\..\..\..\..\samples\ControlCatalog\MainWindow.xaml", diff --git a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj index c59e59be63..4353b7b09c 100644 --- a/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj +++ b/tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1 + net6.0 diff --git a/tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj b/tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj index 42229ba456..636643d9e4 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj +++ b/tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1 + net6.0 diff --git a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj index dd50eff2b6..3b0b082093 100644 --- a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj +++ b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net47 + net6.0 Library true diff --git a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj index a9f5318e8f..f6cfe51db4 100644 --- a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj +++ b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net47 + net6.0 Library true latest diff --git a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj index 74cc6e292b..a3c85c72b5 100644 --- a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj +++ b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1 + net6.0 Library diff --git a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj index 7d1285c025..fceea56273 100644 --- a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj +++ b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net47 + net6.0 Library true diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index 7748115137..14f6525dbe 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net47 + net6.0;net47 Library true latest @@ -25,7 +25,6 @@ - diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs index fb3fd6d7d4..340eac0d4f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs @@ -512,6 +512,33 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions var brush = (ISolidColorBrush)border.Background; Assert.Equal(0xff506070, brush.Color.ToUint32()); } + + [Fact] + public void Automatically_Converts_Color_To_SolidColorBrush_From_Setter() + { + using (StyledWindow()) + { + var xaml = @" + + + #ff506070 + + + + +