diff --git a/.editorconfig b/.editorconfig index a144ec8843..62a533e468 100644 --- a/.editorconfig +++ b/.editorconfig @@ -177,7 +177,9 @@ dotnet_diagnostic.CA1828.severity = warning dotnet_diagnostic.CA1829.severity = warning #CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters dotnet_diagnostic.CA1847.severity = warning -#CACA2211:Non-constant fields should not be visible +#CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method +dotnet_diagnostic.CA1854.severity = warning +#CA2211:Non-constant fields should not be visible dotnet_diagnostic.CA2211.severity = error # Wrapping preferences @@ -186,6 +188,23 @@ csharp_wrap_before_ternary_opsigns = false # Avalonia DevAnalyzer preferences dotnet_diagnostic.AVADEV2001.severity = error +# Avalonia PublicAnalyzer preferences +dotnet_diagnostic.AVP1000.severity = error +dotnet_diagnostic.AVP1001.severity = error +dotnet_diagnostic.AVP1002.severity = error +dotnet_diagnostic.AVP1010.severity = error +dotnet_diagnostic.AVP1011.severity = error +dotnet_diagnostic.AVP1012.severity = warning +dotnet_diagnostic.AVP1013.severity = error +dotnet_diagnostic.AVP1020.severity = error +dotnet_diagnostic.AVP1021.severity = error +dotnet_diagnostic.AVP1022.severity = error +dotnet_diagnostic.AVP1030.severity = error +dotnet_diagnostic.AVP1031.severity = error +dotnet_diagnostic.AVP1032.severity = error +dotnet_diagnostic.AVP1040.severity = error +dotnet_diagnostic.AVA2001.severity = error + # Xaml files [*.{xaml,axaml}] indent_size = 2 diff --git a/.ncrunch/SafeAreaDemo.Android.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.Android.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/SafeAreaDemo.Android.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/SafeAreaDemo.Desktop.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.Desktop.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/SafeAreaDemo.Desktop.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/SafeAreaDemo.iOS.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.iOS.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/SafeAreaDemo.iOS.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/SafeAreaDemo.v3.ncrunchproject b/.ncrunch/SafeAreaDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/SafeAreaDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 875161d336..d2f2ee36d5 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -101,10 +101,6 @@ "type": "boolean", "description": "skip-tests" }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded. Default is Avalonia.sln" - }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 76620e8b93..92c924f107 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -3,6 +3,7 @@ "path": "Avalonia.sln", "projects": [ "packages\\Avalonia\\Avalonia.csproj", + "samples\\AppWithoutLifetime\\AppWithoutLifetime.csproj", "samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj", "samples\\ControlCatalog\\ControlCatalog.csproj", "samples\\GpuInterop\\GpuInterop.csproj", @@ -23,8 +24,6 @@ "src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj", "src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj", "src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj", - "src\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", - "src\\Avalonia.Headless\\Avalonia.Headless.csproj", "src\\Avalonia.MicroCom\\Avalonia.MicroCom.csproj", "src\\Avalonia.Native\\Avalonia.Native.csproj", "src\\Avalonia.OpenGL\\Avalonia.OpenGL.csproj", @@ -33,6 +32,8 @@ "src\\Avalonia.Themes.Fluent\\Avalonia.Themes.Fluent.csproj", "src\\Avalonia.Themes.Simple\\Avalonia.Themes.Simple.csproj", "src\\Avalonia.X11\\Avalonia.X11.csproj", + "src\\Headless\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", + "src\\Headless\\Avalonia.Headless\\Avalonia.Headless.csproj", "src\\Linux\\Avalonia.LinuxFramebuffer\\Avalonia.LinuxFramebuffer.csproj", "src\\Markup\\Avalonia.Markup.Xaml.Loader\\Avalonia.Markup.Xaml.Loader.csproj", "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", @@ -65,4 +66,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} \ No newline at end of file +} diff --git a/Avalonia.sln b/Avalonia.sln index b21df07628..9670327d67 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -181,9 +181,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Headless\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Headless\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" EndProject @@ -233,7 +233,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\R EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\Avalonia.Analyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}" ProjectSection(SolutionItems) = preProject @@ -244,13 +244,32 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppWithoutLifetime", "samples\AppWithoutLifetime\AppWithoutLifetime.csproj", "{F8928267-688E-4A51-989C-612A72446D33}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo", "samples\SafeAreaDemo\SafeAreaDemo.csproj", "{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Android", "samples\SafeAreaDemo.Android\SafeAreaDemo.Android.csproj", "{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Desktop", "samples\SafeAreaDemo.Desktop\SafeAreaDemo.Desktop.csproj", "{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.iOS", "samples\SafeAreaDemo.iOS\SafeAreaDemo.iOS.csproj", "{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF237916-7150-496B-89ED-6CA3292896E7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit", "src\Headless\Avalonia.Headless.NUnit\Avalonia.Headless.NUnit.csproj", "{ED976634-B118-43F8-8B26-0279C7A7044F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.UnitTests", "tests\Avalonia.Headless.NUnit.UnitTests\Avalonia.Headless.NUnit.UnitTests.csproj", "{2999D79E-3C20-4A90-B651-CA7E0AC92D35}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.UnitTests", "tests\Avalonia.Headless.XUnit.UnitTests\Avalonia.Headless.XUnit.UnitTests.csproj", "{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -583,18 +602,62 @@ Global {DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU {DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.Build.0 = Release|Any CPU - {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.Build.0 = Release|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU + {F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.Build.0 = Release|Any CPU + {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.Build.0 = Release|Any CPU + {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.Build.0 = Release|Any CPU + {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.Deploy.0 = Release|Any CPU + {4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Release|Any CPU.Build.0 = Release|Any CPU + {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Build.0 = Release|Any CPU + {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Deploy.0 = Release|Any CPU + {ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.Build.0 = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU + {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Release|Any CPU.Build.0 = Release|Any CPU + {2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Release|Any CPU.Build.0 = Release|Any CPU + {F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -639,6 +702,8 @@ Global {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} {BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098} @@ -661,10 +726,22 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} - {DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} - {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} + {DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {F8928267-688E-4A51-989C-612A72446D33} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {ED976634-B118-43F8-8B26-0279C7A7044F} = {FF237916-7150-496B-89ED-6CA3292896E7} + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} + {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {2999D79E-3C20-4A90-B651-CA7E0AC92D35} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/Directory.Build.targets b/Directory.Build.targets index 73954c7f4d..e8d4baba11 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,5 @@ - + $(DefineConstants);NET7SDK diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props index 7d021d051f..dffd3098c3 100644 --- a/build/DevAnalyzers.props +++ b/build/DevAnalyzers.props @@ -5,7 +5,7 @@ ReferenceOutputAssembly="false" OutputItemType="Analyzer" SetTargetFramework="TargetFramework=netstandard2.0"/> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dirs.proj b/dirs.proj index f1eaae8a4a..d29aa61fcb 100644 --- a/dirs.proj +++ b/dirs.proj @@ -9,10 +9,11 @@ - + - + + diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index fdc144e3a5..86bacfb819 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -12,7 +12,6 @@ { ComPtr _parent; NSTrackingArea* _area; - NSMutableAttributedString* _markedText; bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; AvnInputModifiers _modifierState; NSEvent* _lastMouseDownEvent; @@ -22,8 +21,9 @@ AvnPlatformResizeReason _resizeReason; AvnAccessibilityElement* _accessibilityChild; NSRect _cursorRect; - NSMutableString* _text; - NSRange _selection; + NSMutableAttributedString* _text; + NSRange _selectedRange; + NSRange _markedRange; } - (void)onClosed @@ -59,6 +59,11 @@ [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; _modifierState = AvnInputModifiersNone; + + _text = [[NSMutableAttributedString alloc] initWithString:@""]; + _markedRange = NSMakeRange(0, 0); + _selectedRange = NSMakeRange(0, 0); + return self; } @@ -270,7 +275,7 @@ delta.Y = [event deltaY]; } - uint32 timestamp = static_cast([event timestamp] * 1000); + uint64_t timestamp = static_cast([event timestamp] * 1000); auto modifiers = [self getModifiers:[event modifierFlags]]; if(type != Move || @@ -439,7 +444,7 @@ auto key = s_KeyMap[[event keyCode]]; - uint32_t timestamp = static_cast([event timestamp] * 1000); + uint64_t timestamp = static_cast([event timestamp] * 1000); auto modifiers = [self getModifiers:[event modifierFlags]]; if(_parent != nullptr) @@ -521,9 +526,13 @@ - (void)keyDown:(NSEvent *)event { - [self keyboardEvent:event withType:KeyDown]; - _lastKeyHandled = [[self inputContext] handleEvent:event]; - [super keyDown:event]; + _lastKeyHandled = false; + + [[self inputContext] handleEvent:event]; + + if(!_lastKeyHandled){ + [self keyboardEvent:event withType:KeyDown]; + } } - (void)keyUp:(NSEvent *)event @@ -532,6 +541,10 @@ [super keyUp:event]; } +- (void) doCommandBySelector:(SEL)selector{ + +} + - (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod { unsigned int rv = 0; @@ -561,50 +574,52 @@ - (BOOL)hasMarkedText { - return [_markedText length] > 0; + return _markedRange.length > 0; } - (NSRange)markedRange { - if([_markedText length] > 0) - return NSMakeRange(0, [_markedText length] - 1); - return NSMakeRange(NSNotFound, 0); + return _markedRange; } - (NSRange)selectedRange { - return _selection; + return _selectedRange; } - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { + _lastKeyHandled = true; + + NSString* markedText; + if([string isKindOfClass:[NSAttributedString class]]) { - _markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + markedText = [string string]; } else { - _markedText = [[NSMutableAttributedString alloc] initWithString:string]; + markedText = (NSString*) string; } - if(!_parent->InputMethod->IsActive()){ - return; + _markedRange = NSMakeRange(_selectedRange.location, [markedText length]); + + if(_parent->InputMethod->IsActive()){ + _parent->InputMethod->Client->SetPreeditText((char*)[markedText UTF8String]); } - - _parent->InputMethod->Client->SetPreeditText((char*)[_markedText.string UTF8String]); } - (void)unmarkText { - [[_markedText mutableString] setString:@""]; + if(_parent->InputMethod->IsActive()){ + _parent->InputMethod->Client->SetPreeditText(nullptr); + } - [[self inputContext] discardMarkedText]; + _markedRange = NSMakeRange(_selectedRange.location, 0); - if(!_parent->InputMethod->IsActive()){ - return; + if([self inputContext]) { + [[self inputContext] discardMarkedText]; } - - _parent->InputMethod->Client->SetPreeditText(nullptr); } - (NSArray *)validAttributesForMarkedText @@ -614,19 +629,38 @@ - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange { - return nullptr; + if(actualRange){ + range = *actualRange; + } + + NSAttributedString* subString = [_text attributedSubstringFromRange:range]; + + return subString; } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { - [self unmarkText]; - - if(_parent != nullptr) + if(_parent == nullptr){ + return; + } + + NSString* text; + + if([string isKindOfClass:[NSAttributedString class]]) { - _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); + text = [string string]; + } + else + { + text = (NSString*) string; } - [[self inputContext] invalidateCharacterCoordinates]; + [self unmarkText]; + + uint64_t timestamp = static_cast([NSDate timeIntervalSinceReferenceDate] * 1000); + + _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(timestamp, [text UTF8String]); + } - (NSUInteger)characterIndexForPoint:(NSPoint)point @@ -746,15 +780,11 @@ } - (void) setText:(NSString *)text{ - [_text setString:text]; - - [[self inputContext] discardMarkedText]; + [[_text mutableString] setString:text]; } - (void) setSelection:(int)start :(int)end{ - _selection = NSMakeRange(start, end - start); - - [[self inputContext] invalidateCharacterCoordinates]; + _selectedRange = NSMakeRange(start, end - start); } - (void) setCursorRect:(AvnRect)rect{ @@ -766,7 +796,9 @@ _cursorRect = windowRectOnScreen; - [[self inputContext] invalidateCharacterCoordinates]; + if([self inputContext]) { + [[self inputContext] invalidateCharacterCoordinates]; + } } @end diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 16e1486acc..ef50cdab84 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -460,7 +460,7 @@ auto point = [self translateLocalPoint:avnPoint]; AvnVector delta = { 0, 0 }; - _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, static_cast([event timestamp] * 1000), AvnInputModifiersNone, point, delta); + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, static_cast([event timestamp] * 1000), AvnInputModifiersNone, point, delta); } if(!_isTransitioningToFullScreen) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 40232947d9..87e1e86bf9 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -35,8 +35,6 @@ using MicroCom.CodeGenerator; partial class Build : NukeBuild { - [Solution("Avalonia.sln")] readonly Solution Solution; - BuildParameters Parameters { get; set; } protected override void OnBuildInitialized() { @@ -143,10 +141,12 @@ partial class Build : NukeBuild void RunCoreTest(string projectName) { Information($"Running tests from {projectName}"); - var project = Solution.GetProject(projectName).NotNull("project != null"); + var project = RootDirectory.GlobFiles(@$"**\{projectName}.csproj").FirstOrDefault() + ?? throw new InvalidOperationException($"Project {projectName} doesn't exist"); + // Nuke and MSBuild tools have build-in helpers to get target frameworks from the project. // Unfortunately, it gets broken with every second SDK update, so we had to do it manually. - var fileXml = XDocument.Parse(File.ReadAllText(project.Path)); + var fileXml = XDocument.Parse(File.ReadAllText(project)); var targetFrameworks = fileXml.Descendants("TargetFrameworks") .FirstOrDefault()?.Value.Split(';').Select(f => f.Trim()); if (targetFrameworks is null) @@ -212,6 +212,8 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.Headless.NUnit.UnitTests"); + RunCoreTest("Avalonia.Headless.XUnit.UnitTests"); }); Target RunRenderTests => _ => _ @@ -273,6 +275,8 @@ partial class Build : NukeBuild if(!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, new NumergeNukeLogger())) throw new Exception("Package merge failed"); + RefAssemblyGenerator.GenerateRefAsmsInPackage(Parameters.NugetRoot / "Avalonia." + + Parameters.Version + ".nupkg"); }); Target RunTests => _ => _ @@ -308,7 +312,7 @@ partial class Build : NukeBuild public static int Main() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? Execute(x => x.Package) + ? Execute(x => x.RunToolsTests) : Execute(x => x.RunTests); } diff --git a/nukebuild/Helpers.cs b/nukebuild/Helpers.cs new file mode 100644 index 0000000000..d8d06559bf --- /dev/null +++ b/nukebuild/Helpers.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using Nuke.Common.Utilities; + +class Helpers +{ + public static IDisposable UseTempDir(out string dir) + { + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(path); + dir = path; + return DelegateDisposable.CreateBracket(null, () => + { + try + { + Directory.Delete(path, true); + } + catch + { + // ignore + } + }); + } +} diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs new file mode 100644 index 0000000000..f0d5c81a37 --- /dev/null +++ b/nukebuild/RefAssemblyGenerator.cs @@ -0,0 +1,177 @@ +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using ILRepacking; +using Mono.Cecil; +using Mono.Cecil.Cil; + +public class RefAssemblyGenerator +{ + class Resolver : DefaultAssemblyResolver, IAssemblyResolver + { + private readonly string _dir; + Dictionary _cache = new(); + + public Resolver(string dir) + { + _dir = dir; + } + + public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) + { + if (_cache.TryGetValue(name.Name, out var asm)) + return asm; + var path = Path.Combine(_dir, name.Name + ".dll"); + if (File.Exists(path)) + return _cache[name.Name] = AssemblyDefinition.ReadAssembly(path, parameters); + return base.Resolve(name, parameters); + } + } + + public static void PatchRefAssembly(string file) + { + var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk"); + var snk = new byte[reader.Length]; + reader.Read(snk, 0, snk.Length); + + var def = AssemblyDefinition.ReadAssembly(file, new ReaderParameters + { + ReadWrite = true, + InMemory = true, + ReadSymbols = true, + SymbolReaderProvider = new DefaultSymbolReaderProvider(false), + AssemblyResolver = new Resolver(Path.GetDirectoryName(file)) + }); + + var obsoleteAttribute = def.MainModule.ImportReference(new TypeReference("System", "ObsoleteAttribute", def.MainModule, + def.MainModule.TypeSystem.CoreLibrary)); + var obsoleteCtor = def.MainModule.ImportReference(new MethodReference(".ctor", + def.MainModule.TypeSystem.Void, obsoleteAttribute) + { + Parameters = { new ParameterDefinition(def.MainModule.TypeSystem.String) } + }); + + foreach(var t in def.MainModule.Types) + ProcessType(t, obsoleteCtor); + def.Write(file, new WriterParameters() + { + StrongNameKeyBlob = snk, + WriteSymbols = def.MainModule.HasSymbols, + SymbolWriterProvider = new EmbeddedPortablePdbWriterProvider(), + DeterministicMvid = def.MainModule.HasSymbols + }); + } + + static void ProcessType(TypeDefinition type, MethodReference obsoleteCtor) + { + foreach (var nested in type.NestedTypes) + ProcessType(nested, obsoleteCtor); + if (type.IsInterface) + { + var hideMethods = type.Name.EndsWith("Impl") + || (type.HasCustomAttributes && type.CustomAttributes.Any(a => + a.AttributeType.FullName == "Avalonia.Metadata.PrivateApiAttribute")); + + var injectMethod = hideMethods + || type.CustomAttributes.Any(a => + a.AttributeType.FullName == "Avalonia.Metadata.NotClientImplementableAttribute"); + + if (hideMethods) + { + foreach (var m in type.Methods) + { + var dflags = MethodAttributes.Public | MethodAttributes.Family | MethodAttributes.FamORAssem | + MethodAttributes.FamANDAssem | MethodAttributes.Assembly; + m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly; + } + } + + if(injectMethod) + { + type.Methods.Add(new MethodDefinition("NotClientImplementable", + MethodAttributes.Assembly + | MethodAttributes.Abstract + | MethodAttributes.NewSlot + | MethodAttributes.HideBySig, type.Module.TypeSystem.Void)); + } + + var forceUnstable = type.CustomAttributes.FirstOrDefault(a => + a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute"); + + foreach (var m in type.Methods) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + foreach (var m in type.Properties) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + foreach (var m in type.Events) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + + } + } + + static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute unstableAttribute) + { + if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute")) + return; + + unstableAttribute = def.CustomAttributes.FirstOrDefault(a => + a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute") ?? unstableAttribute; + + if (unstableAttribute is null) + return; + + var message = unstableAttribute.ConstructorArguments.FirstOrDefault().Value?.ToString(); + if (string.IsNullOrEmpty(message)) + { + message = "This is a part of unstable API and can be changed in minor releases. Consider replacing it with alternatives or reach out developers on GitHub."; + } + + def.CustomAttributes.Add(new CustomAttribute(obsoleteCtor) + { + ConstructorArguments = + { + new CustomAttributeArgument(obsoleteCtor.Module.TypeSystem.String, message) + } + }); + } + + public static void GenerateRefAsmsInPackage(string packagePath) + { + using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), + ZipArchiveMode.Update)) + { + foreach (var entry in archive.Entries.ToList()) + { + if (entry.FullName.StartsWith("ref/")) + entry.Delete(); + } + + foreach (var entry in archive.Entries.ToList()) + { + if (entry.FullName.StartsWith("lib/") && entry.Name.EndsWith(".xml")) + { + var newEntry = archive.CreateEntry("ref/" + entry.FullName.Substring(4), + CompressionLevel.Optimal); + using (var src = entry.Open()) + using (var dst = newEntry.Open()) + src.CopyTo(dst); + } + } + + var libs = archive.Entries.Where(e => e.FullName.StartsWith("lib/") && e.FullName.EndsWith(".dll")) + .Select((e => new { s = e.FullName.Split('/'), e = e })) + .Select(e => new { Tfm = e.s[1], Name = e.s[2], Entry = e.e }) + .GroupBy(x => x.Tfm); + foreach(var tfm in libs) + using (Helpers.UseTempDir(out var temp)) + { + foreach (var l in tfm) + l.Entry.ExtractToFile(Path.Combine(temp, l.Name)); + foreach (var l in tfm) + PatchRefAssembly(Path.Combine(temp, l.Name)); + foreach (var l in tfm) + archive.CreateEntryFromFile(Path.Combine(temp, l.Name), $"ref/{l.Tfm}/{l.Name}"); + } + } + } +} diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 13bac4b7db..8999b7ca76 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -15,7 +15,7 @@ - + @@ -31,18 +31,11 @@ - - - - - - - + + - - - + diff --git a/nukebuild/numerge.config b/nukebuild/numerge.config index 09f22ec527..71b77bee93 100644 --- a/nukebuild/numerge.config +++ b/nukebuild/numerge.config @@ -16,6 +16,11 @@ "Id": "Avalonia.Generators", "IgnoreMissingFrameworkBinaries": true, "DoNotMergeDependencies": true + }, + { + "Id": "Avalonia.Analyzers", + "IgnoreMissingFrameworkBinaries": true, + "DoNotMergeDependencies": true } ] } diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index f21d6fefb4..412251bc9c 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,6 +5,7 @@ + all @@ -15,6 +16,10 @@ ReferenceOutputAssembly="false" PrivateAssets="all" OutputItemType="Analyzer" /> + @@ -60,4 +65,20 @@ + + + + $(IntermediateOutputPath)/AvaloniaVersion.props + + + + + true + build + + + diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 2334aa91bc..a2da228887 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -4,9 +4,11 @@ $(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe $(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll false + $(UsedAvaloniaProducts);AvaloniaUI + diff --git a/readme.md b/readme.md index c8135080fe..6dd556bd0d 100644 --- a/readme.md +++ b/readme.md @@ -1,26 +1,43 @@ -[![GH_Banner](https://user-images.githubusercontent.com/552074/218457976-92e76834-9e22-4e35-acfa-aa50281bc0f9.png)](https://avaloniaui.net/xpf) +![Star our repo to show support](https://user-images.githubusercontent.com/552074/235945895-1b896994-a0b6-4e7c-a522-c5688c4ec1b9.png) +![Header](https://user-images.githubusercontent.com/552074/235865745-2a8e7274-4f66-4f77-8f05-feeb76e7d478.png) [![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) -# ⚠️ **v11 Update - Pausing community contributions** - -for more information see [this](https://github.com/AvaloniaUI/Avalonia/discussions/10599) discussion. +⚠️ **v11 Update - [Pausing community contributions](https://github.com/AvaloniaUI/Avalonia/discussions/10599)** ## 📖 About -Avalonia is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS. Avalonia is mature and production ready. We also have in beta release support for iOS, Android and in early stages support for browser via WASM. +[Avalonia](https://avaloniaui.net) is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of platforms such as Windows, macOS, Linux, iOS, Android and WebAssembly. Avalonia is mature and production ready and is used by companies, including [Schneider Electric](https://avaloniaui.net/showcase#se), [Unity](https://avaloniaui.net/showcase#unity), [JetBrains](https://avaloniaui.net/showcase#rider) and [Github](https://avaloniaui.net/showcase#github). + +Considered by many to be the spiritual successor to WPF, Avalonia UI provides a familiar, modern development experience for XAML developers creating cross-platform applications. While Avalonia UI is [similar to WPF](https://docs.avaloniaui.net/misc/wpf), it isn't a 1:1 copy, and you'll find plenty of improvements. -![image](https://user-images.githubusercontent.com/4672627/152126443-932966cf-57e7-4e77-9be6-62463a66b9f8.png) +For those seeking a cross-platform WPF, we have created [Avalonia XPF](https://avaloniaui.net/xpf), enabling WPF applications to run on macOS and Linux with little to no code changes. Avalonia XPF is a commercial product and is licensed per-app, per-platform. -To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. [Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia! +#### Roadmap +To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). + +#### Breaking Changes +You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. + +#### Awesome Avalonia +[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia! ## 🚀 Getting Started +See our [Get Started](https://avaloniaui.net/GettingStarted) guide to begin developing apps with Avalonia UI. + +### Visual Studio The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://docs.avaloniaui.net/docs/getting-started). +### JetBrains Rider +[JetBrains Rider](https://www.jetbrains.com/rider/whatsnew/?mkt_tok=eyJpIjoiTURBNU1HSmhNV0kwTUdFMiIsInQiOiJtNnU2VEc1TlNLa1ZRVkROYmdZYVpYREJsaU1qdUhmS3dxSzRHczdYWHl0RVlTNDMwSFwvNUs3VENTNVM0bVcyNFdaRmVYZzVWTTF1N3VrQWNGTkJreEhlam1hMlB4UVVWcHBGM1dNOUxoXC95YnRQdGgyUXl1YmZCM3h3d3BVWWdBIn0%3D#avalonia-support) now has official support for Avalonia. + +Code completion, inspections and refactorings are supported out of the box, for XAML previewer add `https://plugins.jetbrains.com/plugins/dev/14839` to plugin repositories and install [AvaloniaRider](https://github.com/ForNeVeR/AvaloniaRider) plugin. + +### Avalonia Packages Avalonia is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/ Use these commands in the Package Manager console to install Avalonia manually: @@ -30,31 +47,26 @@ Install-Package Avalonia.Desktop ``` ## Showcase +[![Showcase_Banner](https://user-images.githubusercontent.com/552074/235946124-bf6fda52-0c9f-4730-868b-0de957e5b97b.png)](https://avaloniaui.net/showcase) -Examples of UIs built with Avalonia - -([Lunacy](https://icons8.com/lunacy)) - -![image](https://user-images.githubusercontent.com/4672627/152325740-261c27a3-e6f0-4662-bff7-4796d4940e04.png) -([PlasticSCM](https://www.plasticscm.com/)) -![image](https://user-images.githubusercontent.com/4672627/152326453-14944c4d-33da-4d50-a268-b87f80927adb.png) -([WasabiWallet](https://www.wasabiwallet.io/)) - -## JetBrains Rider - -[JetBrains Rider](https://www.jetbrains.com/rider/whatsnew/?mkt_tok=eyJpIjoiTURBNU1HSmhNV0kwTUdFMiIsInQiOiJtNnU2VEc1TlNLa1ZRVkROYmdZYVpYREJsaU1qdUhmS3dxSzRHczdYWHl0RVlTNDMwSFwvNUs3VENTNVM0bVcyNFdaRmVYZzVWTTF1N3VrQWNGTkJreEhlam1hMlB4UVVWcHBGM1dNOUxoXC95YnRQdGgyUXl1YmZCM3h3d3BVWWdBIn0%3D#avalonia-support) now has official support for Avalonia. - -Code completion, inspections and refactorings are supported out of the box, for XAML previewer add `https://plugins.jetbrains.com/plugins/dev/14839` to plugin repositories and install [AvaloniaRider](https://github.com/ForNeVeR/AvaloniaRider) plugin. +See what others have built with Avalonia UI on our [Showcase](https://avaloniaui.net/Showcase). We welcome submissions! ## Bleeding Edge Builds We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) which tracks the current state of master. Although these packages are less stable than the release on NuGet.org, you'll get all the latest features and bugfixes right away and many of our users actually prefer this feed! -## Documentation +## Learning -Documentation can be found at https://docs.avaloniaui.net. We also have a [tutorial](https://docs.avaloniaui.net/docs/getting-started/programming-with-avalonia) over there for newcomers. +### Documentation +Documentation can be found at https://docs.avaloniaui.net. + +### Tutorials +We also have a [tutorial](https://docs.avaloniaui.net/docs/getting-started/programming-with-avalonia) over there for newcomers. + +### Samples +We have a [range of samples](https://github.com/AvaloniaUI/Avalonia.Samples) to help you get started. ## Building and Using @@ -116,3 +128,8 @@ We have a range of [support plans available](https://avaloniaui.net/support) for ## .NET Foundation This project is supported by the [.NET Foundation](https://dotnetfoundation.org). + +## Avalonia XPF +Unleash the full potential of your existing WPF apps with our cross-platform UI framework, enabling WPF apps to run on macOS and Linux without requiring expensive and risky rewrites. + +[![GH_Banner](https://user-images.githubusercontent.com/552074/218457976-92e76834-9e22-4e35-acfa-aa50281bc0f9.png)](https://avaloniaui.net/xpf) diff --git a/samples/VirtualizationDemo/App.xaml b/samples/AppWithoutLifetime/App.axaml similarity index 52% rename from samples/VirtualizationDemo/App.xaml rename to samples/AppWithoutLifetime/App.axaml index eb5f0e4dca..5f86b8be93 100644 --- a/samples/VirtualizationDemo/App.xaml +++ b/samples/AppWithoutLifetime/App.axaml @@ -1,7 +1,7 @@ - - - + x:Class="AppWithoutLifetime.App"> + + + diff --git a/samples/AppWithoutLifetime/App.axaml.cs b/samples/AppWithoutLifetime/App.axaml.cs new file mode 100644 index 0000000000..9cc99929c4 --- /dev/null +++ b/samples/AppWithoutLifetime/App.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia; +using Avalonia.Markup.Xaml; + +namespace AppWithoutLifetime; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } +} diff --git a/samples/AppWithoutLifetime/AppWithoutLifetime.csproj b/samples/AppWithoutLifetime/AppWithoutLifetime.csproj new file mode 100644 index 0000000000..fce12af298 --- /dev/null +++ b/samples/AppWithoutLifetime/AppWithoutLifetime.csproj @@ -0,0 +1,21 @@ + + + WinExe + net6.0 + enable + app.manifest + + + + + + + + + + + + + + + diff --git a/samples/AppWithoutLifetime/MainWindow.axaml b/samples/AppWithoutLifetime/MainWindow.axaml new file mode 100644 index 0000000000..3f31cb7fae --- /dev/null +++ b/samples/AppWithoutLifetime/MainWindow.axaml @@ -0,0 +1,13 @@ + + + + + + - + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index c9c7939c1c..7130b3602a 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -202,7 +202,7 @@ namespace IntegrationTestApp { var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; - foreach (var window in lifetime.Windows) + foreach (var window in lifetime.Windows.ToArray()) { window.Activate(); } @@ -212,7 +212,7 @@ namespace IntegrationTestApp { var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; - foreach (var window in lifetime.Windows) + foreach (var window in lifetime.Windows.ToArray()) { window.Show(); if (window.WindowState == WindowState.Minimized) @@ -270,6 +270,8 @@ namespace IntegrationTestApp this.Get("BasicListBox").SelectedIndex = -1; if (source?.Name == "MenuClickedMenuItemReset") this.Get("ClickedMenuItem").Text = "None"; + if (source?.Name == "ResetSliders") + this.Get("HorizontalSlider").Value = 50; if (source?.Name == "ShowTransparentWindow") ShowTransparentWindow(); if (source?.Name == "ShowTransparentPopup") diff --git a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj index a24e55de81..31a6b05175 100644 --- a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj +++ b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj @@ -19,7 +19,7 @@ - + diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index 3f89a9d5f7..d764af8978 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -308,6 +308,41 @@ + + @@ -332,6 +367,11 @@ + + + Drop + Shadow + diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index 4a3e20ff5b..2d64ba8f7b 100644 --- a/samples/RenderDemo/Pages/CustomSkiaPage.cs +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -44,9 +44,9 @@ namespace RenderDemo.Pages public bool HitTest(Point p) => false; public bool Equals(ICustomDrawOperation other) => false; static Stopwatch St = Stopwatch.StartNew(); - public void Render(IDrawingContextImpl context) + public void Render(ImmediateDrawingContext context) { - var leaseFeature = context.GetFeature(); + var leaseFeature = context.TryGetFeature(); if (leaseFeature == null) context.DrawGlyphRun(Brushes.Black, _noSkia.PlatformImpl); else diff --git a/samples/RenderDemo/Pages/WriteableBitmapPage.cs b/samples/RenderDemo/Pages/WriteableBitmapPage.cs index 850e398a93..a13a625d14 100644 --- a/samples/RenderDemo/Pages/WriteableBitmapPage.cs +++ b/samples/RenderDemo/Pages/WriteableBitmapPage.cs @@ -59,7 +59,7 @@ namespace RenderDemo.Pages color = new Color(fillAlpha, r, g, b); } - data[y * fb.Size.Width + x] = (int) color.ToUint32(); + data[y * fb.Size.Width + x] = (int) color.ToUInt32(); } } diff --git a/samples/SafeAreaDemo.Android/Icon.png b/samples/SafeAreaDemo.Android/Icon.png new file mode 100644 index 0000000000..41a2a618fb Binary files /dev/null and b/samples/SafeAreaDemo.Android/Icon.png differ diff --git a/samples/SafeAreaDemo.Android/MainActivity.cs b/samples/SafeAreaDemo.Android/MainActivity.cs new file mode 100644 index 0000000000..b0f0a6e419 --- /dev/null +++ b/samples/SafeAreaDemo.Android/MainActivity.cs @@ -0,0 +1,11 @@ +using Android.App; +using Android.Content.PM; +using Avalonia.Android; + +namespace SafeAreaDemo.Android +{ + [Activity(Label = "SafeAreaDemo.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] + public class MainActivity : AvaloniaMainActivity + { + } +} diff --git a/samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml b/samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml new file mode 100644 index 0000000000..b6a5777e03 --- /dev/null +++ b/samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/SafeAreaDemo.Android/Resources/drawable/splash_screen.xml b/samples/SafeAreaDemo.Android/Resources/drawable/splash_screen.xml new file mode 100644 index 0000000000..2e920b4b3b --- /dev/null +++ b/samples/SafeAreaDemo.Android/Resources/drawable/splash_screen.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/samples/SafeAreaDemo.Android/Resources/values/colors.xml b/samples/SafeAreaDemo.Android/Resources/values/colors.xml new file mode 100644 index 0000000000..59279d5d32 --- /dev/null +++ b/samples/SafeAreaDemo.Android/Resources/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/samples/SafeAreaDemo.Android/Resources/values/styles.xml b/samples/SafeAreaDemo.Android/Resources/values/styles.xml new file mode 100644 index 0000000000..2759d2904a --- /dev/null +++ b/samples/SafeAreaDemo.Android/Resources/values/styles.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj b/samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj new file mode 100644 index 0000000000..f5d2af79d0 --- /dev/null +++ b/samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj @@ -0,0 +1,24 @@ + + + Exe + net7.0-android + 21 + enable + com.avalonia.safeareademo + 1 + 1.0 + apk + False + + + + + Resources\drawable\Icon.png + + + + + + + + diff --git a/samples/SafeAreaDemo.Android/SplashActivity.cs b/samples/SafeAreaDemo.Android/SplashActivity.cs new file mode 100644 index 0000000000..621ad1c675 --- /dev/null +++ b/samples/SafeAreaDemo.Android/SplashActivity.cs @@ -0,0 +1,30 @@ +using Android.App; +using Android.Content; +using Android.OS; +using Avalonia; +using Avalonia.Android; +using Application = Android.App.Application; + +namespace SafeAreaDemo.Android +{ + [Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)] + public class SplashActivity : AvaloniaSplashActivity + { + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder); + } + + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + } + + protected override void OnResume() + { + base.OnResume(); + + StartActivity(new Intent(Application.Context, typeof(MainActivity))); + } + } +} diff --git a/samples/SafeAreaDemo.Desktop/Program.cs b/samples/SafeAreaDemo.Desktop/Program.cs new file mode 100644 index 0000000000..b07682e8c8 --- /dev/null +++ b/samples/SafeAreaDemo.Desktop/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace SafeAreaDemo.Desktop +{ + internal class Program + { + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); + } +} diff --git a/samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj b/samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj new file mode 100644 index 0000000000..a3b020d531 --- /dev/null +++ b/samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj @@ -0,0 +1,24 @@ + + + WinExe + + net7.0 + enable + true + + + + app.manifest + + + + + + + + + + + + diff --git a/samples/SafeAreaDemo.Desktop/app.manifest b/samples/SafeAreaDemo.Desktop/app.manifest new file mode 100644 index 0000000000..f0a4b00175 --- /dev/null +++ b/samples/SafeAreaDemo.Desktop/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/samples/SafeAreaDemo.iOS/AppDelegate.cs b/samples/SafeAreaDemo.iOS/AppDelegate.cs new file mode 100644 index 0000000000..6990435d78 --- /dev/null +++ b/samples/SafeAreaDemo.iOS/AppDelegate.cs @@ -0,0 +1,17 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.iOS; +using Avalonia.Media; +using Foundation; +using UIKit; + +namespace SafeAreaDemo.iOS +{ + // The UIApplicationDelegate for the application. This class is responsible for launching the + // User Interface of the application, as well as listening (and optionally responding) to + // application events from iOS. + [Register("AppDelegate")] + public partial class AppDelegate : AvaloniaAppDelegate + { + } +} diff --git a/samples/SafeAreaDemo.iOS/Entitlements.plist b/samples/SafeAreaDemo.iOS/Entitlements.plist new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/samples/SafeAreaDemo.iOS/Entitlements.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/SafeAreaDemo.iOS/Info.plist b/samples/SafeAreaDemo.iOS/Info.plist new file mode 100644 index 0000000000..ec04bd5a87 --- /dev/null +++ b/samples/SafeAreaDemo.iOS/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDisplayName + SafeAreaDemo + CFBundleIdentifier + companyName.SafeAreaDemo + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 10.0 + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIStatusBarHidden + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/samples/SafeAreaDemo.iOS/Main.cs b/samples/SafeAreaDemo.iOS/Main.cs new file mode 100644 index 0000000000..1c76dc6bc4 --- /dev/null +++ b/samples/SafeAreaDemo.iOS/Main.cs @@ -0,0 +1,15 @@ +using UIKit; + +namespace SafeAreaDemo.iOS +{ + public class Application + { + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } + } +} \ No newline at end of file diff --git a/samples/SafeAreaDemo.iOS/Resources/LaunchScreen.xib b/samples/SafeAreaDemo.iOS/Resources/LaunchScreen.xib new file mode 100644 index 0000000000..c6dd636c46 --- /dev/null +++ b/samples/SafeAreaDemo.iOS/Resources/LaunchScreen.xib @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/SafeAreaDemo.iOS/SafeAreaDemo.iOS.csproj b/samples/SafeAreaDemo.iOS/SafeAreaDemo.iOS.csproj new file mode 100644 index 0000000000..71365fe07d --- /dev/null +++ b/samples/SafeAreaDemo.iOS/SafeAreaDemo.iOS.csproj @@ -0,0 +1,18 @@ + + + Exe + net7.0-ios + 10.0 + manual + enable + iossimulator-x64 + + + + + + + + + + diff --git a/samples/SafeAreaDemo/App.xaml b/samples/SafeAreaDemo/App.xaml new file mode 100644 index 0000000000..f5ffbdb32a --- /dev/null +++ b/samples/SafeAreaDemo/App.xaml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/samples/SafeAreaDemo/App.xaml.cs b/samples/SafeAreaDemo/App.xaml.cs new file mode 100644 index 0000000000..e23cb0e04a --- /dev/null +++ b/samples/SafeAreaDemo/App.xaml.cs @@ -0,0 +1,36 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using SafeAreaDemo.ViewModels; +using SafeAreaDemo.Views; + +namespace SafeAreaDemo +{ + public partial class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow + { + DataContext = new MainViewModel() + }; + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + singleViewPlatform.MainView = new MainView + { + DataContext = new MainViewModel() + }; + } + + base.OnFrameworkInitializationCompleted(); + } + } +} \ No newline at end of file diff --git a/samples/SafeAreaDemo/Assets/avalonia-logo.ico b/samples/SafeAreaDemo/Assets/avalonia-logo.ico new file mode 100644 index 0000000000..da8d49ff9b Binary files /dev/null and b/samples/SafeAreaDemo/Assets/avalonia-logo.ico differ diff --git a/samples/SafeAreaDemo/SafeAreaDemo.csproj b/samples/SafeAreaDemo/SafeAreaDemo.csproj new file mode 100644 index 0000000000..20bc5ec8fe --- /dev/null +++ b/samples/SafeAreaDemo/SafeAreaDemo.csproj @@ -0,0 +1,27 @@ + + + net7.0 + enable + latest + true + + + + + + %(Filename) + + + Designer + + + + + + + + + + + + diff --git a/samples/SafeAreaDemo/ViewLocator.cs b/samples/SafeAreaDemo/ViewLocator.cs new file mode 100644 index 0000000000..4f71fdbe9c --- /dev/null +++ b/samples/SafeAreaDemo/ViewLocator.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using MiniMvvm; + +namespace SafeAreaDemo +{ + public class ViewLocator : IDataTemplate + { + public Control? Build(object? data) + { + if (data is null) + return null; + + var name = data.GetType().FullName!.Replace("ViewModel", "View"); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } + } +} diff --git a/samples/SafeAreaDemo/ViewModels/MainViewModel.cs b/samples/SafeAreaDemo/ViewModels/MainViewModel.cs new file mode 100644 index 0000000000..fe58567171 --- /dev/null +++ b/samples/SafeAreaDemo/ViewModels/MainViewModel.cs @@ -0,0 +1,112 @@ +using Avalonia; +using Avalonia.Controls.Platform; +using MiniMvvm; + +namespace SafeAreaDemo.ViewModels +{ + public class MainViewModel : ViewModelBase + { + private bool _useSafeArea = true; + private bool _fullscreen; + private IInsetsManager? _insetsManager; + private bool _hideSystemBars; + + public Thickness SafeAreaPadding + { + get + { + return _insetsManager?.SafeAreaPadding ?? default; + } + } + + public Thickness ViewPadding + { + get + { + return _useSafeArea ? SafeAreaPadding : default; + } + } + + public bool UseSafeArea + { + get => _useSafeArea; + set + { + _useSafeArea = value; + + this.RaisePropertyChanged(); + + RaiseSafeAreaChanged(); + } + } + + public bool Fullscreen + { + get => _fullscreen; + set + { + _fullscreen = value; + + if (_insetsManager != null) + { + _insetsManager.DisplayEdgeToEdge = value; + } + + this.RaisePropertyChanged(); + + RaiseSafeAreaChanged(); + } + } + + public bool HideSystemBars + { + get => _hideSystemBars; + set + { + _hideSystemBars = value; + + if (_insetsManager != null) + { + _insetsManager.IsSystemBarVisible = !value; + } + + this.RaisePropertyChanged(); + + RaiseSafeAreaChanged(); + } + } + + internal IInsetsManager? InsetsManager + { + get => _insetsManager; + set + { + if (_insetsManager != null) + { + _insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged; + } + + _insetsManager = value; + + if (_insetsManager != null) + { + _insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged; + + _insetsManager.DisplayEdgeToEdge = _fullscreen; + _insetsManager.IsSystemBarVisible = !_hideSystemBars; + } + } + } + + private void InsetsManager_SafeAreaChanged(object? sender, SafeAreaChangedArgs e) + { + RaiseSafeAreaChanged(); + } + + private void RaiseSafeAreaChanged() + { + this.RaisePropertyChanged(nameof(SafeAreaPadding)); + this.RaisePropertyChanged(nameof(ViewPadding)); + } + } +} diff --git a/samples/SafeAreaDemo/Views/MainView.xaml b/samples/SafeAreaDemo/Views/MainView.xaml new file mode 100644 index 0000000000..a8f7c2e735 --- /dev/null +++ b/samples/SafeAreaDemo/Views/MainView.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + Fullscreen + Use Safe Area + Hide System Bars + + + + + + + diff --git a/samples/SafeAreaDemo/Views/MainView.xaml.cs b/samples/SafeAreaDemo/Views/MainView.xaml.cs new file mode 100644 index 0000000000..2b651225e7 --- /dev/null +++ b/samples/SafeAreaDemo/Views/MainView.xaml.cs @@ -0,0 +1,25 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using SafeAreaDemo.ViewModels; + +namespace SafeAreaDemo.Views +{ + public partial class MainView : UserControl + { + public MainView() + { + AvaloniaXamlLoader.Load(this); + } + + protected override void OnLoaded() + { + base.OnLoaded(); + + var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager; + if (insetsManager != null && DataContext is MainViewModel viewModel) + { + viewModel.InsetsManager = insetsManager; + } + } + } +} diff --git a/samples/SafeAreaDemo/Views/MainWindow.xaml b/samples/SafeAreaDemo/Views/MainWindow.xaml new file mode 100644 index 0000000000..ccd3028bb9 --- /dev/null +++ b/samples/SafeAreaDemo/Views/MainWindow.xaml @@ -0,0 +1,12 @@ + + + diff --git a/samples/SafeAreaDemo/Views/MainWindow.xaml.cs b/samples/SafeAreaDemo/Views/MainWindow.xaml.cs new file mode 100644 index 0000000000..de8f2b05ca --- /dev/null +++ b/samples/SafeAreaDemo/Views/MainWindow.xaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace SafeAreaDemo.Views +{ + public partial class MainWindow : Window + { + public MainWindow() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs index 23d45edf6a..e3dda25b29 100644 --- a/samples/Sandbox/MainWindow.axaml.cs +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -1,22 +1,17 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Input.TextInput; using Avalonia.Markup.Xaml; using Avalonia.Win32.WinRT.Composition; namespace Sandbox { - public class MainWindow : Window + public partial class MainWindow : Window { public MainWindow() { - this.InitializeComponent(); - this.AttachDevTools(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); + InitializeComponent(); } } } diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index f23e391a2a..d2e66988e0 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -4,6 +4,7 @@ WinExe net6.0 true + true @@ -17,4 +18,5 @@ + diff --git a/samples/VirtualizationDemo/App.axaml b/samples/VirtualizationDemo/App.axaml new file mode 100644 index 0000000000..f5f06ffb6a --- /dev/null +++ b/samples/VirtualizationDemo/App.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/App.axaml.cs b/samples/VirtualizationDemo/App.axaml.cs new file mode 100644 index 0000000000..5ac5c9a92b --- /dev/null +++ b/samples/VirtualizationDemo/App.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace VirtualizationDemo; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = new MainWindow(); + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/samples/VirtualizationDemo/App.xaml.cs b/samples/VirtualizationDemo/App.xaml.cs deleted file mode 100644 index 81b80c1f40..0000000000 --- a/samples/VirtualizationDemo/App.xaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; - -namespace VirtualizationDemo -{ - public class App : Application - { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - desktop.MainWindow = new MainWindow(); - base.OnFrameworkInitializationCompleted(); - } - } -} diff --git a/samples/VirtualizationDemo/Assets/chat.json b/samples/VirtualizationDemo/Assets/chat.json new file mode 100644 index 0000000000..cc628b534a --- /dev/null +++ b/samples/VirtualizationDemo/Assets/chat.json @@ -0,0 +1,190 @@ +{ + "chat": [ + { + "sender": "Alice", + "message": "Hey Bob! How was your weekend?", + "timestamp": "2023-04-01T10:00:00" + }, + { + "sender": "Bob", + "message": "It was great, thanks for asking. I went on a camping trip with some friends. How about you?", + "timestamp": "2023-04-01T10:01:00" + }, + { + "sender": "Alice", + "message": "My weekend was pretty chill. I just stayed home and caught up on some TV shows.", + "timestamp": "2023-04-01T10:03:00" + }, + { + "sender": "Bob", + "message": "That sounds relaxing. What shows did you watch?", + "timestamp": "2023-04-01T10:05:00" + }, + { + "sender": "Alice", + "message": "I watched the new season of 'Stranger Things' and started watching 'Ozark'. Have you seen them?", + "timestamp": "2023-04-01T10:07:00" + }, + { + "sender": "Bob", + "message": "Yeah, I've seen both of those. They're really good! What do you think of them so far?", + "timestamp": "2023-04-01T10:10:00" + }, + { + "sender": "Alice", + "message": "I'm really enjoying 'Stranger Things', but 'Ozark' is a bit darker than I expected. I'm only a few episodes in though, so we'll see how it goes.", + "timestamp": "2023-04-01T10:12:00" + }, + { + "sender": "Bob", + "message": "Yeah, 'Ozark' can be intense at times, but it's really well done. Keep watching, it gets even better.", + "timestamp": "2023-04-01T10:15:00" + }, + { + "sender": "Alice", + "message": "Thanks for the recommendation, I'll definitely keep watching. So, how's work been for you lately?", + "timestamp": "2023-04-01T10:20:00" + }, + { + "sender": "Bob", + "message": "It's been pretty busy, but I'm managing. How about you?", + "timestamp": "2023-04-01T10:22:00" + }, + { + "sender": "Alice", + "message": "Same here, things have been pretty hectic. But it keeps us on our toes, right?", + "timestamp": "2023-04-01T10:25:00" + }, + { + "sender": "Bob", + "message": "Absolutely. Hey, have you heard about the new project we're starting next week?", + "timestamp": "2023-04-01T10:30:00" + }, + { + "sender": "Alice", + "message": "No, I haven't. What's it about?", + "timestamp": "2023-04-01T10:32:00" + }, + { + "sender": "Bob", + "message": "It's a big project for a new client, and it's going to require a lot of extra hours from all of us. But the pay is going to be great,so it's definitely worth the extra effort. I'll fill you in on the details later, but for now, let's just enjoy our coffee break, shall we?", + "timestamp": "2023-04-01T10:35:00" + }, + { + "sender": "Alice", + "message": "Sounds good to me. I could use a break right about now.", + "timestamp": "2023-04-01T10:40:00" + }, + { + "sender": "Bob", + "message": "Me too. So, have you tried the new caf� down the street yet?", + "timestamp": "2023-04-01T10:45:00" + }, + { + "sender": "Alice", + "message": "No, I haven't. Is it any good?", + "timestamp": "2023-04-01T10:47:00" + }, + { + "sender": "Bob", + "message": "It's really good! They have the best croissants I've ever tasted.", + "timestamp": "2023-04-01T10:50:00" + }, + { + "sender": "Alice", + "message": "Hmm, I'll have to try it out sometime. Do they have any vegan options?", + "timestamp": "2023-04-01T10:52:00" + }, + { + "sender": "Bob", + "message": "I'm not sure, but I think they do. You should ask them the next time you go there.", + "timestamp": "2023-04-01T10:55:00" + }, + { + "sender": "Alice", + "message": "Thanks for the suggestion. I'm always looking for good vegan options around here.", + "timestamp": "2023-04-01T11:00:00" + }, + { + "sender": "Bob", + "message": "No problem. So, have you made any plans for the weekend yet?", + "timestamp": "2023-04-01T11:05:00" + }, + { + "sender": "Alice", + "message": "Not yet. I was thinking of maybe going for a hike or something. What about you?", + "timestamp": "2023-04-01T11:07:00" + }, + { + "sender": "Bob", + "message": "I haven't made any plans either. Maybe we could do something together?", + "timestamp": "2023-04-01T11:10:00" + }, + { + "sender": "Alice", + "message": "That sounds like a great idea! Let's plan on it.", + "timestamp": "2023-04-01T11:12:00" + }, + { + "sender": "Bob", + "message": "Awesome. I'll check out some hiking trails and let you know which ones look good.", + "timestamp": "2023-04-01T11:15:00" + }, + { + "sender": "Alice", + "message": "Sounds good. I can't wait!", + "timestamp": "2023-04-01T11:20:00" + }, + { + "sender": "John", + "message": "Hey Lisa, how was your day?", + "timestamp": "2023-04-01T18:00:00" + }, + { + "sender": "Lisa", + "message": "It was good, thanks for asking. How about you?", + "timestamp": "2023-04-01T18:05:00" + }, + { + "sender": "John", + "message": "Eh, it was alright. Work was pretty busy, but nothing too crazy.", + "timestamp": "2023-04-01T18:10:00" + }, + { + "sender": "Lisa", + "message": "Yeah, I know what you mean. My boss has been on my case lately about meeting our deadlines.", + "timestamp": "2023-04-01T18:15:00" + }, + { + "sender": "John", + "message": "That sucks. Are you feeling stressed out?", + "timestamp": "2023-04-01T18:20:00" + }, + { + "sender": "Lisa", + "message": "A little bit, yeah. But I'm trying to stay positive and focus on getting my work done.", + "timestamp": "2023-04-01T18:25:00" + }, + { + "sender": "John", + "message": "That's a good attitude to have. Have you tried doing some meditation or other relaxation techniques?", + "timestamp": "2023-04-01T18:30:00" + }, + { + "sender": "Lisa", + "message": "I haven't, but I've been thinking about it. Do you have any suggestions?", + "timestamp": "2023-04-01T18:35:00" + }, + { + "sender": "John", + "message": "Sure, I could send you some links to guided meditations that I've found helpful. And there are also some great apps out there that can help you with relaxation.", + "timestamp": "2023-04-01T18:40:00" + }, + { + "sender": "Lisa", + "message": "That would be awesome, thanks so much!", + "timestamp": "2023-04-01T18:45:00" + } + ] +} + diff --git a/samples/VirtualizationDemo/MainWindow.axaml b/samples/VirtualizationDemo/MainWindow.axaml new file mode 100644 index 0000000000..04e75450bf --- /dev/null +++ b/samples/VirtualizationDemo/MainWindow.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/MainWindow.axaml.cs b/samples/VirtualizationDemo/MainWindow.axaml.cs new file mode 100644 index 0000000000..533dc00aa1 --- /dev/null +++ b/samples/VirtualizationDemo/MainWindow.axaml.cs @@ -0,0 +1,15 @@ +using Avalonia; +using Avalonia.Controls; +using VirtualizationDemo.ViewModels; + +namespace VirtualizationDemo; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + this.AttachDevTools(); + DataContext = new MainWindowViewModel(); + } +} diff --git a/samples/VirtualizationDemo/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml deleted file mode 100644 index 3aee63c246..0000000000 --- a/samples/VirtualizationDemo/MainWindow.xaml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - Horiz. ScrollBar - - Vert. ScrollBar - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/VirtualizationDemo/MainWindow.xaml.cs b/samples/VirtualizationDemo/MainWindow.xaml.cs deleted file mode 100644 index cea200dcec..0000000000 --- a/samples/VirtualizationDemo/MainWindow.xaml.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using VirtualizationDemo.ViewModels; - -namespace VirtualizationDemo -{ - public class MainWindow : Window - { - public MainWindow() - { - this.InitializeComponent(); - this.AttachDevTools(); - DataContext = new MainWindowViewModel(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/samples/VirtualizationDemo/Models/Chat.cs b/samples/VirtualizationDemo/Models/Chat.cs new file mode 100644 index 0000000000..813e8650f5 --- /dev/null +++ b/samples/VirtualizationDemo/Models/Chat.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace VirtualizationDemo.Models; + +public class ChatFile +{ + public ChatMessage[]? Chat { get; set; } + + public static ChatFile Load(string path) + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + using var s = File.OpenRead(path); + return JsonSerializer.Deserialize(s, options)!; + } +} + +public record ChatMessage(string Sender, string Message, DateTimeOffset Timestamp); diff --git a/samples/VirtualizationDemo/Program.cs b/samples/VirtualizationDemo/Program.cs index febda46450..87212b6daa 100644 --- a/samples/VirtualizationDemo/Program.cs +++ b/samples/VirtualizationDemo/Program.cs @@ -1,15 +1,14 @@ using Avalonia; -namespace VirtualizationDemo +namespace VirtualizationDemo; + +class Program { - class Program - { - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace(); + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); - public static int Main(string[] args) - => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } + public static int Main(string[] args) + => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } diff --git a/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs new file mode 100644 index 0000000000..c0abe62bd5 --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using VirtualizationDemo.Models; + +namespace VirtualizationDemo.ViewModels; + +public class ChatPageViewModel +{ + public ChatPageViewModel() + { + var chat = ChatFile.Load(Path.Combine("Assets", "chat.json")); + Messages = new(chat.Chat ?? Array.Empty()); + } + + public ObservableCollection Messages { get; } +} diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs new file mode 100644 index 0000000000..a17fc2d303 --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs @@ -0,0 +1,21 @@ +using MiniMvvm; + +namespace VirtualizationDemo.ViewModels; + +public class ExpanderItemViewModel : ViewModelBase +{ + private string? _header; + private bool _isExpanded; + + public string? Header + { + get => _header; + set => RaiseAndSetIfChanged(ref _header, value); + } + + public bool IsExpanded + { + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); + } +} diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs new file mode 100644 index 0000000000..f2807a803b --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs @@ -0,0 +1,17 @@ +using System.Collections.ObjectModel; +using System.Linq; + +namespace VirtualizationDemo.ViewModels; + +internal class ExpanderPageViewModel +{ + public ExpanderPageViewModel() + { + Items = new(Enumerable.Range(0, 100).Select(x => new ExpanderItemViewModel + { + Header = $"Item {x}", + })); + } + + public ObservableCollection Items { get; set; } +} diff --git a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs deleted file mode 100644 index 9ba505ffe5..0000000000 --- a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using MiniMvvm; - -namespace VirtualizationDemo.ViewModels -{ - internal class ItemViewModel : ViewModelBase - { - private string _prefix; - private int _index; - private double _height = double.NaN; - - public ItemViewModel(int index, string prefix = "Item") - { - _prefix = prefix; - _index = index; - } - - public string Header => $"{_prefix} {_index}"; - - public double Height - { - get => _height; - set => this.RaiseAndSetIfChanged(ref _height, value); - } - } -} diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 96dbbc1a83..478e40187e 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -1,160 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Layout; -using Avalonia.Controls.Selection; -using MiniMvvm; +using MiniMvvm; -namespace VirtualizationDemo.ViewModels -{ - internal class MainWindowViewModel : ViewModelBase - { - private int _itemCount = 200; - private string _newItemString = "New Item"; - private int _newItemIndex; - private AvaloniaList _items; - private string _prefix = "Item"; - private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto; - private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto; - private Orientation _orientation = Orientation.Vertical; - - public MainWindowViewModel() - { - this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); - RecreateCommand = MiniCommand.Create(() => Recreate()); - - AddItemCommand = MiniCommand.Create(() => AddItem()); - - RemoveItemCommand = MiniCommand.Create(() => Remove()); - - SelectFirstCommand = MiniCommand.Create(() => SelectItem(0)); - - SelectLastCommand = MiniCommand.Create(() => SelectItem(Items.Count - 1)); - } - - public string NewItemString - { - get { return _newItemString; } - set { this.RaiseAndSetIfChanged(ref _newItemString, value); } - } - - public int ItemCount - { - get { return _itemCount; } - set { this.RaiseAndSetIfChanged(ref _itemCount, value); } - } - - public SelectionModel Selection { get; } = new SelectionModel(); - - public AvaloniaList Items - { - get { return _items; } - private set { this.RaiseAndSetIfChanged(ref _items, value); } - } - - public Orientation Orientation - { - get { return _orientation; } - set { this.RaiseAndSetIfChanged(ref _orientation, value); } - } - - public IEnumerable Orientations => - Enum.GetValues(typeof(Orientation)).Cast(); - - public ScrollBarVisibility HorizontalScrollBarVisibility - { - get { return _horizontalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _horizontalScrollBarVisibility, value); } - } +namespace VirtualizationDemo.ViewModels; - public ScrollBarVisibility VerticalScrollBarVisibility - { - get { return _verticalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _verticalScrollBarVisibility, value); } - } - - public IEnumerable ScrollBarVisibilities => - Enum.GetValues(typeof(ScrollBarVisibility)).Cast(); - - public MiniCommand AddItemCommand { get; private set; } - public MiniCommand RecreateCommand { get; private set; } - public MiniCommand RemoveItemCommand { get; private set; } - public MiniCommand SelectFirstCommand { get; private set; } - public MiniCommand SelectLastCommand { get; private set; } - - public void RandomizeSize() - { - var random = new Random(); - - foreach (var i in Items) - { - i.Height = random.Next(240) + 10; - } - } - - public void ResetSize() - { - foreach (var i in Items) - { - i.Height = double.NaN; - } - } - - private void ResizeItems(int count) - { - if (Items == null) - { - var items = Enumerable.Range(0, count) - .Select(x => new ItemViewModel(x)); - Items = new AvaloniaList(items); - } - else if (count > Items.Count) - { - var items = Enumerable.Range(Items.Count, count - Items.Count) - .Select(x => new ItemViewModel(x)); - Items.AddRange(items); - } - else if (count < Items.Count) - { - Items.RemoveRange(count, Items.Count - count); - } - } - - private void AddItem() - { - var index = Items.Count; - - if (Selection.SelectedItems.Count > 0) - { - index = Selection.SelectedIndex; - } - - Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); - } - - private void Remove() - { - if (Selection.SelectedItems.Count > 0) - { - Items.RemoveAll(Selection.SelectedItems.ToList()); - } - } - - private void Recreate() - { - _prefix = _prefix == "Item" ? "Recreated" : "Item"; - var items = Enumerable.Range(0, _itemCount) - .Select(x => new ItemViewModel(x, _prefix)); - Items = new AvaloniaList(items); - } - - private void SelectItem(int index) - { - Selection.SelectedIndex = index; - } - } +internal class MainWindowViewModel : ViewModelBase +{ + public PlaygroundPageViewModel Playground { get; } = new(); + public ChatPageViewModel Chat { get; } = new(); + public ExpanderPageViewModel Expanders { get; } = new(); } diff --git a/samples/VirtualizationDemo/ViewModels/PlaygroundItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/PlaygroundItemViewModel.cs new file mode 100644 index 0000000000..584ef4600b --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/PlaygroundItemViewModel.cs @@ -0,0 +1,17 @@ +using MiniMvvm; + +namespace VirtualizationDemo.ViewModels; + +public class PlaygroundItemViewModel : ViewModelBase +{ + private string? _header; + + public PlaygroundItemViewModel(int index) => Header = $"Item {index}"; + public PlaygroundItemViewModel(string? header) => Header = header; + + public string? Header + { + get => _header; + set => RaiseAndSetIfChanged(ref _header, value); + } +} diff --git a/samples/VirtualizationDemo/ViewModels/PlaygroundPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/PlaygroundPageViewModel.cs new file mode 100644 index 0000000000..98ab91b0a6 --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/PlaygroundPageViewModel.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Selection; +using MiniMvvm; + +namespace VirtualizationDemo.ViewModels; + +public class PlaygroundPageViewModel : ViewModelBase +{ + private SelectionMode _selectionMode = SelectionMode.Multiple; + private int _scrollToIndex = 500; + private string? _newItemHeader = "New Item 1"; + + public PlaygroundPageViewModel() + { + Items = new(Enumerable.Range(0, 1000).Select(x => new PlaygroundItemViewModel(x))); + Selection = new(); + } + + public ObservableCollection Items { get; } + + public bool Multiple + { + get => _selectionMode.HasAnyFlag(SelectionMode.Multiple); + set => SetSelectionMode(SelectionMode.Multiple, value); + } + + public bool Toggle + { + get => _selectionMode.HasAnyFlag(SelectionMode.Toggle); + set => SetSelectionMode(SelectionMode.Toggle, value); + } + + public bool AlwaysSelected + { + get => _selectionMode.HasAnyFlag(SelectionMode.AlwaysSelected); + set => SetSelectionMode(SelectionMode.AlwaysSelected, value); + } + + public SelectionModel Selection { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set => RaiseAndSetIfChanged(ref _selectionMode, value); + } + + public int ScrollToIndex + { + get => _scrollToIndex; + set => RaiseAndSetIfChanged(ref _scrollToIndex, value); + } + + public string? NewItemHeader + { + get => _newItemHeader; + set => RaiseAndSetIfChanged(ref _newItemHeader, value); + } + + public void ExecuteScrollToIndex() + { + Selection.Select(ScrollToIndex); + } + + public void RandomizeScrollToIndex() + { + var rnd = new Random(); + ScrollToIndex = rnd.Next(Items.Count); + } + + public void AddAtSelectedIndex() + { + if (Selection.SelectedIndex == -1) + return; + Items.Insert(Selection.SelectedIndex, new(NewItemHeader)); + } + + public void DeleteSelectedItem() + { + var count = Selection.Count; + for (var i = count - 1; i >= 0; i--) + Items.RemoveAt(Selection.SelectedIndexes[i]); + } + + private void SetSelectionMode(SelectionMode mode, bool value) + { + if (value) + SelectionMode |= mode; + else + SelectionMode &= ~mode; + } +} diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml b/samples/VirtualizationDemo/Views/ChatPageView.axaml new file mode 100644 index 0000000000..fc182f15ae --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs new file mode 100644 index 0000000000..b5c90db69c --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace VirtualizationDemo.Views; + +public partial class ChatPageView : UserControl +{ + public ChatPageView() + { + InitializeComponent(); + } +} diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml new file mode 100644 index 0000000000..972d885229 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs new file mode 100644 index 0000000000..df3689cf24 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VirtualizationDemo.Views; + +public partial class ExpanderPageView : UserControl +{ + public ExpanderPageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml b/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml new file mode 100644 index 0000000000..52bc6fd27a --- /dev/null +++ b/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml @@ -0,0 +1,66 @@ + + + + + + + + Multiple + Toggle + AlwaysSelected + AutoScrollToSelectedItem + WrapSelection + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml.cs b/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml.cs new file mode 100644 index 0000000000..5282475778 --- /dev/null +++ b/samples/VirtualizationDemo/Views/PlaygroundPageView.axaml.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Threading; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; + +namespace VirtualizationDemo.Views; + +public partial class PlaygroundPageView : UserControl +{ + private DispatcherTimer _timer; + + public PlaygroundPageView() + { + InitializeComponent(); + + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(500), + }; + + _timer.Tick += TimerTick; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _timer.Start(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _timer.Stop(); + } + + private void TimerTick(object? sender, EventArgs e) + { + var message = $"Realized {list.GetRealizedContainers().Count()} of {list.ItemsPanelRoot?.Children.Count}"; + itemCount.Text = message; + } +} diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index 81b30c6cbe..3ac7aab589 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -1,19 +1,24 @@  - Exe + WinExe net6.0 + true + + + - - + + + + + PreserveNewest + - - - - - + + diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index 549815a036..251a177432 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -81,7 +81,7 @@ namespace Avalonia.Android.Platform var renderScaling = _topLevel.RenderScaling; var inset = insets.GetInsets( - (DisplayEdgeToEdge ? + (_displayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime()); @@ -91,8 +91,8 @@ namespace Avalonia.Android.Platform return new Thickness(inset.Left / renderScaling, inset.Top / renderScaling, inset.Right / renderScaling, - (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ? - imeInset.Bottom - navBarInset.Bottom : + (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !_displayEdgeToEdge) || !_usesLegacyLayouts) ? + imeInset.Bottom - (_displayEdgeToEdge ? 0 : navBarInset.Bottom) : inset.Bottom) / renderScaling); } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 126c488d59..fae1aacf61 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -91,7 +91,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } @@ -156,12 +156,12 @@ namespace Avalonia.Android.Platform.SkiaPlatform protected virtual void OnResized(Size size) { - Resized?.Invoke(size, PlatformResizeReason.Unspecified); + Resized?.Invoke(size, WindowResizeReason.Unspecified); } internal void Resize(Size size) { - Resized?.Invoke(size, PlatformResizeReason.Layout); + Resized?.Invoke(size, WindowResizeReason.Layout); } class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs index d62acc0d52..dd99c40cd3 100644 --- a/src/Avalonia.Base/Animation/Animation.cs +++ b/src/Avalonia.Base/Animation/Animation.cs @@ -200,7 +200,7 @@ namespace Avalonia.Animation /// /// The animation setter. /// The property animator value. - public static void SetAnimator(IAnimationSetter setter, + public static void SetAnimator(IAnimationSetter setter, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicMethods)] Type value) { @@ -319,7 +319,7 @@ namespace Avalonia.Animation if (animators.Count == 1) { var subscription = animators[0].Apply(this, control, clock, match, onComplete); - + if (subscription is not null) { subscriptions.Add(subscription); @@ -348,9 +348,11 @@ namespace Avalonia.Animation if (onComplete != null) { - Task.WhenAll(completionTasks!).ContinueWith( - (_, state) => ((Action)state!).Invoke(), - onComplete); + Task.WhenAll(completionTasks!) + .ContinueWith((_, state) => ((Action)state!).Invoke() + , onComplete + , TaskScheduler.FromCurrentSynchronizationContext() + ); } } return new CompositeDisposable(subscriptions); diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 639c27bf03..eafff3b780 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Avalonia.Base/ClassBindingManager.cs b/src/Avalonia.Base/ClassBindingManager.cs index a9726cb86e..55f3a7892a 100644 --- a/src/Avalonia.Base/ClassBindingManager.cs +++ b/src/Avalonia.Base/ClassBindingManager.cs @@ -17,6 +17,8 @@ namespace Avalonia return target.Bind(prop, source, anchor); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001:The same AvaloniaProperty should not be registered twice", + Justification = "Classes.attr binding feature is implemented using intermediate avalonia properties for each class")] private static AvaloniaProperty RegisterClassProxyProperty(string className) { var prop = AvaloniaProperty.Register("__AvaloniaReserved::Classes::" + className); diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs index e350a019d4..8c731c188f 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs @@ -35,7 +35,7 @@ namespace Avalonia.Collections /// Indicates if a weak subscription should be used to track changes to the collection. /// /// A disposable used to terminate the subscription. - internal static IDisposable ForEachItem( + public static IDisposable ForEachItem( this IAvaloniaReadOnlyDictionary collection, Action added, Action removed, diff --git a/src/Avalonia.Base/Controls/IResourceDictionary.cs b/src/Avalonia.Base/Controls/IResourceDictionary.cs index 2bd1f65638..6712498bf4 100644 --- a/src/Avalonia.Base/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/IResourceDictionary.cs @@ -18,6 +18,6 @@ namespace Avalonia.Controls /// /// Gets a collection of merged resource dictionaries that are specifically keyed and composed to address theme scenarios. /// - IDictionary ThemeDictionaries { get; } + IDictionary ThemeDictionaries { get; } } } diff --git a/src/Avalonia.Base/Controls/IThemeVariantProvider.cs b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs new file mode 100644 index 0000000000..03a7fb1206 --- /dev/null +++ b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs @@ -0,0 +1,21 @@ +using Avalonia.Metadata; +using Avalonia.Styling; + +namespace Avalonia.Controls; + +/// +/// Resource provider with theme variant awareness. +/// Can be used with . +/// +/// +/// This is a helper interface for the XAML compiler to make Key property accessibly by the markup extensions. +/// Which means, it can only be used with ResourceDictionaries and markup extensions in the XAML code. +/// +[Unstable("This XAML-only API might be removed in the future minor updates.")] +public interface IThemeVariantProvider : IResourceProvider +{ + /// + /// Key property set by the compiler. + /// + ThemeVariant? Key { get; set; } +} diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 231a19baab..b928cf0672 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -13,13 +13,13 @@ namespace Avalonia.Controls /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : IResourceDictionary + public class ResourceDictionary : IResourceDictionary, IThemeVariantProvider { private object? lastDeferredItemKey; private Dictionary? _inner; private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; - private AvaloniaDictionary? _themeDictionary; + private AvaloniaDictionary? _themeDictionary; /// /// Initializes a new instance of the class. @@ -93,13 +93,13 @@ namespace Avalonia.Controls } } - public IDictionary ThemeDictionaries + public IDictionary ThemeDictionaries { get { if (_themeDictionary == null) { - _themeDictionary = new AvaloniaDictionary(2); + _themeDictionary = new AvaloniaDictionary(2); _themeDictionary.ForEachItem( (_, x) => { @@ -120,6 +120,8 @@ namespace Avalonia.Controls return _themeDictionary; } } + + ThemeVariant? IThemeVariantProvider.Key { get; set; } bool IResourceNode.HasResources { @@ -192,7 +194,7 @@ namespace Avalonia.Controls if (_themeDictionary is not null) { - IResourceProvider? themeResourceProvider; + IThemeVariantProvider? themeResourceProvider; if (theme is not null && theme != ThemeVariant.Default) { if (_themeDictionary.TryGetValue(theme, out themeResourceProvider) diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 8aed1545a5..c307b709fe 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -119,7 +119,19 @@ namespace Avalonia.Controls resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); key = key ?? throw new ArgumentNullException(nameof(key)); - return new FloatingResourceObservable(resourceProvider, key, converter); + return new FloatingResourceObservable(resourceProvider, key, null, converter); + } + + public static IObservable GetResourceObservable( + this IResourceProvider resourceProvider, + object key, + ThemeVariant? defaultThemeVariant, + Func? converter = null) + { + resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + return new FloatingResourceObservable(resourceProvider, key, defaultThemeVariant, converter); } private class ResourceObservable : LightweightObservableBase @@ -128,7 +140,10 @@ namespace Avalonia.Controls private readonly object _key; private readonly Func? _converter; - public ResourceObservable(IResourceHost target, object key, Func? converter) + public ResourceObservable( + IResourceHost target, + object key, + Func? converter) { _target = target; _key = key; @@ -170,11 +185,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (_target is not IThemeVariantHost themeVariantHost - || !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) - { - value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; - } + var theme = (_target as IThemeVariantHost)?.ActualThemeVariant; + var value = _target.FindResource(theme, _key) ?? AvaloniaProperty.UnsetValue; return _converter?.Invoke(value) ?? value; } @@ -183,14 +195,20 @@ namespace Avalonia.Controls private class FloatingResourceObservable : LightweightObservableBase { private readonly IResourceProvider _target; + private readonly ThemeVariant? _overrideThemeVariant; private readonly object _key; private readonly Func? _converter; private IResourceHost? _owner; - public FloatingResourceObservable(IResourceProvider target, object key, Func? converter) + public FloatingResourceObservable( + IResourceProvider target, + object key, + ThemeVariant? overrideThemeVariant, + Func? converter) { _target = target; _key = key; + _overrideThemeVariant = overrideThemeVariant; _converter = converter; } @@ -203,11 +221,25 @@ namespace Avalonia.Controls { _owner.ResourcesChanged += ResourcesChanged; } + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost) + { + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; + } } protected override void Deinitialize() { _target.OwnerChanged -= OwnerChanged; + + if (_owner is not null) + { + _owner.ResourcesChanged -= ResourcesChanged; + } + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost) + { + themeVariantHost.ActualThemeVariantChanged -= ActualThemeVariantChanged; + } + _owner = null; } @@ -233,9 +265,9 @@ namespace Avalonia.Controls { _owner.ResourcesChanged -= ResourcesChanged; } - if (_owner is IThemeVariantHost themeVariantHost) + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost) { - themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; + themeVariantHost.ActualThemeVariantChanged -= ActualThemeVariantChanged; } _owner = _target.Owner; @@ -244,12 +276,11 @@ namespace Avalonia.Controls { _owner.ResourcesChanged += ResourcesChanged; } - if (_owner is IThemeVariantHost themeVariantHost2) + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost2) { - themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged; + themeVariantHost2.ActualThemeVariantChanged += ActualThemeVariantChanged; } - PublishNext(); } @@ -265,11 +296,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (!(_target.Owner is IThemeVariantHost themeVariantHost) - || !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) - { - value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; - } + var theme = _overrideThemeVariant ?? (_target.Owner as IThemeVariantHost)?.ActualThemeVariant; + var value = _target.Owner?.FindResource(theme, _key) ?? AvaloniaProperty.UnsetValue; return _converter?.Invoke(value) ?? value; } diff --git a/src/Avalonia.Base/Data/BindingPriority.cs b/src/Avalonia.Base/Data/BindingPriority.cs index 5fd5aae43b..cb7f559e0a 100644 --- a/src/Avalonia.Base/Data/BindingPriority.cs +++ b/src/Avalonia.Base/Data/BindingPriority.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Avalonia.Data { @@ -47,7 +48,7 @@ namespace Avalonia.Data /// Unset = int.MaxValue, - [Obsolete("Use Template priority")] + [Obsolete("Use Template priority"), EditorBrowsable(EditorBrowsableState.Never)] TemplatedParent = Template, } } diff --git a/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs index 5307ea4b9a..88cd1f38af 100644 --- a/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs @@ -35,7 +35,12 @@ namespace Avalonia.Data.Converters public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { value = Inner?.Convert(value, targetType, parameter, culture) ?? value; - return string.Format(culture, Format, value); + var format = Format!; + if (!format.Contains('{')) + { + format = $"{{0:{format}}}"; + } + return string.Format(culture, format, value); } /// diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs index ba5f59ea23..bc300386b9 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties on that have s. /// - internal class DataAnnotationsValidationPlugin : IDataValidationPlugin + public class DataAnnotationsValidationPlugin : IDataValidationPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index e60a341309..2bb8da2c74 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -7,7 +7,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties that report errors by throwing exceptions. /// - internal class ExceptionValidationPlugin : IDataValidationPlugin + public class ExceptionValidationPlugin : IDataValidationPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 3384a99333..87a2f67ee8 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties on objects that implement . /// - internal class IndeiValidationPlugin : IDataValidationPlugin + public class IndeiValidationPlugin : IDataValidationPlugin { private static readonly WeakEvent ErrorsChangedWeakEvent = WeakEvent.Register( diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index c09c31632e..f93813c0b2 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Reactive; using ObservableEx = Avalonia.Reactive.Observable; @@ -49,7 +50,7 @@ namespace Avalonia.Data /// public IObservable Source { get; } - [Obsolete("Use Source property")] + [Obsolete("Use Source property"), EditorBrowsable(EditorBrowsableState.Never)] public IObservable Observable => Source; /// diff --git a/src/Avalonia.Base/Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs index 13ca140565..2bd9fce947 100644 --- a/src/Avalonia.Base/Input/AccessKeyHandler.cs +++ b/src/Avalonia.Base/Input/AccessKeyHandler.cs @@ -176,7 +176,7 @@ namespace Avalonia.Input { bool menuIsOpen = MainMenu?.IsOpen == true; - if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) || menuIsOpen) + if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) && !e.KeyModifiers.HasAllFlags(KeyModifiers.Control) || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. diff --git a/src/Avalonia.Base/Input/Cursor.cs b/src/Avalonia.Base/Input/Cursor.cs index c555087879..49660e508e 100644 --- a/src/Avalonia.Base/Input/Cursor.cs +++ b/src/Avalonia.Base/Input/Cursor.cs @@ -42,19 +42,21 @@ namespace Avalonia.Input public class Cursor : IDisposable { public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow); + private string _name; - internal Cursor(ICursorImpl platformImpl) + private Cursor(ICursorImpl platformImpl, string name) { PlatformImpl = platformImpl; + _name = name; } public Cursor(StandardCursorType cursorType) - : this(GetCursorFactory().GetCursor(cursorType)) + : this(GetCursorFactory().GetCursor(cursorType), cursorType.ToString()) { } public Cursor(IBitmap cursor, PixelPoint hotSpot) - : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot)) + : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot), "BitmapCursor") { } @@ -73,5 +75,10 @@ namespace Avalonia.Input { return AvaloniaLocator.Current.GetRequiredService(); } + + public override string ToString() + { + return _name; + } } } diff --git a/src/Avalonia.Base/Input/DataFormats.cs b/src/Avalonia.Base/Input/DataFormats.cs index 35d50e669a..f593ed205f 100644 --- a/src/Avalonia.Base/Input/DataFormats.cs +++ b/src/Avalonia.Base/Input/DataFormats.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Avalonia.Input { @@ -17,7 +18,7 @@ namespace Avalonia.Input /// /// Dataformat for one or more filenames /// - [Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms.")] + [Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms."), EditorBrowsable(EditorBrowsableState.Never)] public static readonly string FileNames = nameof(FileNames); } } diff --git a/src/Avalonia.Base/Input/DataObjectExtensions.cs b/src/Avalonia.Base/Input/DataObjectExtensions.cs index 6af531b0d8..d2e525cd68 100644 --- a/src/Avalonia.Base/Input/DataObjectExtensions.cs +++ b/src/Avalonia.Base/Input/DataObjectExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using Avalonia.Platform.Storage; @@ -25,7 +26,7 @@ namespace Avalonia.Input /// /// Collection of file names. If format isn't available, returns null. /// - [System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")] + [System.Obsolete("Use GetFiles, this method is supported only on desktop platforms."), EditorBrowsable(EditorBrowsableState.Never)] public static IEnumerable? GetFileNames(this IDataObject dataObject) { return (dataObject.Get(DataFormats.FileNames) as IEnumerable) diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs index 8d7cc2b9a1..26ec98361b 100644 --- a/src/Avalonia.Base/Input/DragEventArgs.cs +++ b/src/Avalonia.Base/Input/DragEventArgs.cs @@ -25,8 +25,7 @@ namespace Avalonia.Input return _target.TranslatePoint(_targetLocation, relativeTo) ?? new Point(0, 0); } - [Unstable] - [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using DragDrop.DoDragDrop or IHeadlessWindow.DragDrop.")] + [Unstable("This constructor might be removed in 12.0. For unit testing, consider using DragDrop.DoDragDrop or IHeadlessWindow.DragDrop.")] public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) : base(routedEvent) { diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs index 2bf666af44..c8de7267ca 100644 --- a/src/Avalonia.Base/Input/FocusManager.cs +++ b/src/Avalonia.Base/Input/FocusManager.cs @@ -122,6 +122,11 @@ namespace Avalonia.Input { scope = scope ?? throw new ArgumentNullException(nameof(scope)); + if (element is not null && !CanFocus(element)) + { + return; + } + if (_focusScopes.TryGetValue(scope, out var existingElement)) { if (element != existingElement) @@ -242,6 +247,6 @@ namespace Avalonia.Input } } - private static bool IsVisible(IInputElement e) => (e as Visual)?.IsVisible ?? true; + private static bool IsVisible(IInputElement e) => (e as Visual)?.IsEffectivelyVisible ?? true; } } diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 962c7aa334..33ddbaedf9 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -647,6 +647,10 @@ namespace Avalonia.Input { PseudoClasses.Set(":focus-within", change.GetNewValue()); } + else if (change.Property == IsVisibleProperty && !change.GetNewValue() && IsFocused) + { + FocusManager.Instance?.Focus(null); + } } /// diff --git a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs index c405cdfacd..3c4562edf4 100644 --- a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs @@ -9,8 +9,7 @@ namespace Avalonia.Input { public Vector Delta { get; } - [Unstable] - [Obsolete("This constructor might be removed in 12.0.")] + [Unstable("This constructor might be removed in 12.0.")] public PointerDeltaEventArgs(RoutedEvent routedEvent, object? source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 28a3c3aefb..7f82199b56 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -14,8 +14,7 @@ namespace Avalonia.Input private readonly PointerPointProperties _properties; private readonly Lazy?>? _previousPoints; - [Unstable] - [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] + [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] public PointerEventArgs(RoutedEvent routedEvent, object? source, IPointer pointer, @@ -77,14 +76,14 @@ namespace Avalonia.Input /// /// Gets the pointer position relative to a control. /// - /// The control. + /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// The pointer position in the control's coordinates. public Point GetPosition(Visual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); /// /// Returns the PointerPoint associated with the current event /// - /// The visual which coordinate system to use. Pass null for toplevel coordinate system + /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// public PointerPoint GetCurrentPoint(Visual? relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); @@ -129,8 +128,7 @@ namespace Avalonia.Input public class PointerPressedEventArgs : PointerEventArgs { - [Unstable] - [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] + [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] public PointerPressedEventArgs( object source, IPointer pointer, @@ -150,8 +148,7 @@ namespace Avalonia.Input public class PointerReleasedEventArgs : PointerEventArgs { - [Unstable] - [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] + [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] public PointerReleasedEventArgs( object source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, @@ -173,8 +170,7 @@ namespace Avalonia.Input { public IPointer Pointer { get; } - [Unstable] - [Obsolete("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.,")] + [Unstable("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.,")] public PointerCaptureLostEventArgs(object source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) { Pointer = pointer; diff --git a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs index 903019d85d..22624a61dd 100644 --- a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs @@ -9,8 +9,7 @@ namespace Avalonia.Input { public Vector Delta { get; } - [Unstable] - [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow.MouseWheel.")] + [Unstable("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow.MouseWheel.")] public PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) diff --git a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs index 9b5668bf98..1c61334888 100644 --- a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs +++ b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs @@ -48,9 +48,9 @@ namespace Avalonia.Input.TextInput } _transformTracker.SetVisual(_client?.TextViewVisual); - UpdateCursorRect(); _im?.SetClient(_client); + UpdateCursorRect(); } else { diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index e16be3fa85..7873f83edb 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -2,6 +2,7 @@ using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Avalonia.Logging; using Avalonia.Rendering; using Avalonia.Threading; @@ -16,10 +17,11 @@ namespace Avalonia.Layout /// public class LayoutManager : ILayoutManager, IDisposable { - private const int MaxPasses = 3; + private const int MaxPasses = 10; private readonly Layoutable _owner; private readonly LayoutQueue _toMeasure = new LayoutQueue(v => !v.IsMeasureValid); private readonly LayoutQueue _toArrange = new LayoutQueue(v => !v.IsArrangeValid); + private readonly List _toArrangeAfterMeasure = new(); private readonly Action _executeLayoutPass; private List? _effectiveViewportChangedListeners; private bool _disposed; @@ -64,7 +66,6 @@ namespace Avalonia.Layout } _toMeasure.Enqueue(control); - _toArrange.Enqueue(control); QueueLayoutPass(); } @@ -249,10 +250,12 @@ namespace Avalonia.Layout { var control = _toMeasure.Dequeue(); - if (!control.IsMeasureValid && control.IsAttachedToVisualTree) + if (!control.IsMeasureValid) { Measure(control); } + + _toArrange.Enqueue(control); } } @@ -262,11 +265,16 @@ namespace Avalonia.Layout { var control = _toArrange.Dequeue(); - if (!control.IsArrangeValid && control.IsAttachedToVisualTree) + if (!control.IsArrangeValid) { - Arrange(control); + if (Arrange(control) == ArrangeResult.AncestorMeasureInvalid) + _toArrangeAfterMeasure.Add(control); } } + + foreach (var i in _toArrangeAfterMeasure) + InvalidateArrange(i); + _toArrangeAfterMeasure.Clear(); } private bool Measure(Layoutable control) @@ -302,18 +310,21 @@ namespace Avalonia.Layout return true; } - private bool Arrange(Layoutable control) + private ArrangeResult Arrange(Layoutable control) { if (!control.IsVisible || !control.IsAttachedToVisualTree) - return false; + return ArrangeResult.NotVisible; if (control.VisualParent is Layoutable parent) { - if (!Arrange(parent)) - return false; + if (Arrange(parent) is var parentResult && parentResult != ArrangeResult.Arranged) + return parentResult; } - if (control.IsMeasureValid && !control.IsArrangeValid) + if (!control.IsMeasureValid) + return ArrangeResult.AncestorMeasureInvalid; + + if (!control.IsArrangeValid) { if (control is IEmbeddedLayoutRoot embeddedRoot) control.Arrange(new Rect(embeddedRoot.AllocatedSize)); @@ -327,7 +338,7 @@ namespace Avalonia.Layout } } - return true; + return ArrangeResult.Arranged; } private void QueueLayoutPass() @@ -430,5 +441,12 @@ namespace Avalonia.Layout public Layoutable Listener { get; } public Rect Viewport { get; set; } } + + private enum ArrangeResult + { + Arranged, + NotVisible, + AncestorMeasureInvalid, + } } } diff --git a/src/Avalonia.Base/Layout/LayoutQueue.cs b/src/Avalonia.Base/Layout/LayoutQueue.cs index 24adeb0793..48efa501f2 100644 --- a/src/Avalonia.Base/Layout/LayoutQueue.cs +++ b/src/Avalonia.Base/Layout/LayoutQueue.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using Avalonia.Logging; namespace Avalonia.Layout { @@ -48,10 +49,21 @@ namespace Avalonia.Layout { _loopQueueInfo.TryGetValue(item, out var info); - if (!info.Active && info.Count < _maxEnqueueCountPerLoop) + if (!info.Active) { - _inner.Enqueue(item); - _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 }; + if (info.Count < _maxEnqueueCountPerLoop) + { + _inner.Enqueue(item); + _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 }; + } + else + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Layout)?.Log( + this, + "Layout cycle detected. Item {Item} was enqueued {Count} times.", + item, + info.Count); + } } } diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index ed88b73149..08f327d048 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -776,10 +776,24 @@ namespace Avalonia.Layout // All changes to visibility cause the parent element to be notified. this.GetVisualParent()?.ChildDesiredSizeChanged(this); - // We only invalidate outselves when visibility is changed to true. if (change.GetNewValue()) { + // We only invalidate ourselves when visibility is changed to true. InvalidateMeasure(); + + // If any descendant had its measure/arrange invalidated while we were hidden, + // they will need to to be registered with the layout manager now that they + // are again effectively visible. If IsEffectivelyVisible becomes an observable + // property then we can piggy-pack on that; for the moment we do this manually. + if (VisualRoot is ILayoutRoot layoutRoot) + { + var count = VisualChildren.Count; + + for (var i = 0; i < count; ++i) + { + (VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutRoot.LayoutManager); + } + } } } } @@ -804,6 +818,30 @@ namespace Avalonia.Layout InvalidateMeasure(); } + private void AncestorBecameVisible(ILayoutManager layoutManager) + { + if (!IsVisible) + return; + + if (!IsMeasureValid) + { + layoutManager.InvalidateMeasure(this); + InvalidateVisual(); + } + else if (!IsArrangeValid) + { + layoutManager.InvalidateArrange(this); + InvalidateVisual(); + } + + var count = VisualChildren.Count; + + for (var i = 0; i < count; ++i) + { + (VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutManager); + } + } + /// /// Called when the layout manager raises a LayoutUpdated event. /// diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index f06f272e51..50c2faacc0 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -6,11 +6,12 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using System.ComponentModel; using System.Globalization; #if !BUILDTASK using Avalonia.Animation.Animators; -using static Avalonia.Utilities.SpanHelpers; #endif +using static Avalonia.Utilities.SpanHelpers; namespace Avalonia.Media { @@ -449,7 +450,7 @@ namespace Avalonia.Media /// public override string ToString() { - uint rgb = ToUint32(); + uint rgb = ToUInt32(); return KnownColors.GetKnownColorName(rgb) ?? $"#{rgb.ToString("x8", CultureInfo.InvariantCulture)}"; } @@ -459,11 +460,18 @@ namespace Avalonia.Media /// /// The integer representation of the color. /// - public uint ToUint32() + public uint ToUInt32() { return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B; } + /// + [Obsolete("Use Color.ToUInt32() instead."), EditorBrowsable(EditorBrowsableState.Never)] + public uint ToUint32() + { + return ToUInt32(); + } + /// /// Returns the HSL color model equivalent of this RGB color. /// diff --git a/src/Avalonia.Base/Media/DashStyle.cs b/src/Avalonia.Base/Media/DashStyle.cs index 4749bfa401..2529b9317d 100644 --- a/src/Avalonia.Base/Media/DashStyle.cs +++ b/src/Avalonia.Base/Media/DashStyle.cs @@ -44,6 +44,8 @@ namespace Avalonia.Media /// /// The dashes collection. /// The dash sequence offset. + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", + Justification = "Collection properties shouldn't be set with SetCurrentValue.")] public DashStyle(IEnumerable? dashes, double offset) { Dashes = (dashes as AvaloniaList) ?? new AvaloniaList(dashes ?? Array.Empty()); diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index f2106f2f86..02294368c5 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; using Avalonia.Utilities; -using Avalonia.Media.Imaging; namespace Avalonia.Media { @@ -53,12 +53,10 @@ namespace Avalonia.Media /// The image. /// The rect in the image to draw. /// The rect in the output to draw to. - /// The bitmap interpolation mode. - public virtual void DrawImage(IImage source, Rect sourceRect, Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode = default) + public virtual void DrawImage(IImage source, Rect sourceRect, Rect destRect) { _ = source ?? throw new ArgumentNullException(nameof(source)); - source.Draw(this, sourceRect, destRect, bitmapInterpolationMode); + source.Draw(this, sourceRect, destRect); } /// @@ -68,8 +66,7 @@ namespace Avalonia.Media /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - /// The bitmap interpolation mode. - internal abstract void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); + internal abstract void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect); /// /// Draws a line. @@ -132,7 +129,7 @@ namespace Avalonia.Media double radiusX = 0, double radiusY = 0, BoxShadows boxShadows = default) { - if (brush == null && !PenIsVisible(pen)) + if (brush == null && !PenIsVisible(pen) && boxShadows.Count == 0) return; if (!MathUtilities.IsZero(radiusX)) { @@ -160,7 +157,7 @@ namespace Avalonia.Media /// public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) { - if (brush == null && !PenIsVisible(pen)) + if (brush == null && !PenIsVisible(pen) && boxShadows.Count == 0) return; DrawRectangleCore(brush, pen, rrect, boxShadows); } @@ -286,8 +283,7 @@ namespace Avalonia.Media Opacity, Clip, GeometryClip, - OpacityMask, - BitmapBlendMode + OpacityMask } public RestoreState(DrawingContext context, PushedStateType type) @@ -312,8 +308,6 @@ namespace Avalonia.Media _context.PopGeometryClipCore(); else if (_type == PushedStateType.OpacityMask) _context.PopOpacityMaskCore(); - else if (_type == PushedStateType.BitmapBlendMode) - _context.PopBitmapBlendModeCore(); } } @@ -394,16 +388,6 @@ namespace Avalonia.Media } protected abstract void PushOpacityMaskCore(IBrush mask, Rect bounds); - public PushedState PushBitmapBlendMode(BitmapBlendingMode blendingMode) - { - PushBitmapBlendMode(blendingMode); - _states ??= StateStackPool.Get(); - _states.Push(new RestoreState(this, RestoreState.PushedStateType.BitmapBlendMode)); - return new PushedState(this); - } - - protected abstract void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode); - /// /// Pushes a matrix transformation. /// @@ -417,11 +401,11 @@ namespace Avalonia.Media return new PushedState(this); } - [Obsolete("Use PushTransform")] + [Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)] public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix); - [Obsolete("Use PushTransform")] + [Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)] public PushedState PushPostTransform(Matrix matrix) => PushTransform(matrix); - [Obsolete("Use PushTransform")] + [Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)] public PushedState PushTransformContainer() => PushTransform(Matrix.Identity); @@ -431,7 +415,6 @@ namespace Avalonia.Media protected abstract void PopGeometryClipCore(); protected abstract void PopOpacityCore(); protected abstract void PopOpacityMaskCore(); - protected abstract void PopBitmapBlendModeCore(); protected abstract void PopTransformCore(); private static bool PenIsVisible(IPen? pen) diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index c96d2aad57..5a5bd50c7c 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -196,13 +196,7 @@ namespace Avalonia.Media throw new NotImplementedException(); } - protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) - { - throw new NotImplementedException(); - } - - internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) { throw new NotImplementedException(); } @@ -321,8 +315,6 @@ namespace Avalonia.Media protected override void PopOpacityMaskCore() => Pop(); - protected override void PopBitmapBlendModeCore() => Pop(); - protected override void PopTransformCore() => Pop(); /// diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs index 52fbd87db7..c83e8eb6ee 100644 --- a/src/Avalonia.Base/Media/DrawingImage.cs +++ b/src/Avalonia.Base/Media/DrawingImage.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Metadata; -using Avalonia.Media.Imaging; namespace Avalonia.Media { @@ -43,8 +42,7 @@ namespace Avalonia.Media void IImage.Draw( DrawingContext context, Rect sourceRect, - Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode) + Rect destRect) { var drawing = Drawing; diff --git a/src/Avalonia.Base/Media/EdgeMode.cs b/src/Avalonia.Base/Media/EdgeMode.cs new file mode 100644 index 0000000000..f50a2f7164 --- /dev/null +++ b/src/Avalonia.Base/Media/EdgeMode.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Media +{ + public enum EdgeMode : byte + { + Unspecified, + + Antialias, + Aliased + } +} diff --git a/src/Avalonia.Base/Media/Effects/BlurEffect.cs b/src/Avalonia.Base/Media/Effects/BlurEffect.cs new file mode 100644 index 0000000000..47c86e4e42 --- /dev/null +++ b/src/Avalonia.Base/Media/Effects/BlurEffect.cs @@ -0,0 +1,22 @@ +using System; +// ReSharper disable CheckNamespace +namespace Avalonia.Media; + +public class BlurEffect : Effect, IBlurEffect, IMutableEffect +{ + public static readonly StyledProperty RadiusProperty = AvaloniaProperty.Register( + nameof(Radius), 5); + + public double Radius + { + get => GetValue(RadiusProperty); + set => SetValue(RadiusProperty, value); + } + + static BlurEffect() + { + AffectsRender(RadiusProperty); + } + + public IImmutableEffect ToImmutable() => new ImmutableBlurEffect(Radius); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/DropShadowEffect.cs b/src/Avalonia.Base/Media/Effects/DropShadowEffect.cs new file mode 100644 index 0000000000..ea931c0a8c --- /dev/null +++ b/src/Avalonia.Base/Media/Effects/DropShadowEffect.cs @@ -0,0 +1,104 @@ +// ReSharper disable once CheckNamespace + +using System; +// ReSharper disable CheckNamespace + +namespace Avalonia.Media; + +public abstract class DropShadowEffectBase : Effect +{ + public static readonly StyledProperty BlurRadiusProperty = + AvaloniaProperty.Register( + nameof(BlurRadius), 5); + + public double BlurRadius + { + get => GetValue(BlurRadiusProperty); + set => SetValue(BlurRadiusProperty, value); + } + + public static readonly StyledProperty ColorProperty = AvaloniaProperty.Register( + nameof(Color), Colors.Black); + + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + public static readonly StyledProperty OpacityProperty = + AvaloniaProperty.Register( + nameof(Opacity), 1); + + public double Opacity + { + get => GetValue(OpacityProperty); + set => SetValue(OpacityProperty, value); + } + + static DropShadowEffectBase() + { + AffectsRender(BlurRadiusProperty, ColorProperty, OpacityProperty); + } +} + +public class DropShadowEffect : DropShadowEffectBase, IDropShadowEffect, IMutableEffect +{ + public static readonly StyledProperty OffsetXProperty = AvaloniaProperty.Register( + nameof(OffsetX), 3.5355); + + public double OffsetX + { + get => GetValue(OffsetXProperty); + set => SetValue(OffsetXProperty, value); + } + + public static readonly StyledProperty OffsetYProperty = AvaloniaProperty.Register( + nameof(OffsetY), 3.5355); + + public double OffsetY + { + get => GetValue(OffsetYProperty); + set => SetValue(OffsetYProperty, value); + } + + static DropShadowEffect() + { + AffectsRender(OffsetXProperty, OffsetYProperty); + } + + public IImmutableEffect ToImmutable() + { + return new ImmutableDropShadowEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity); + } +} + +/// +/// This class is compatible with WPF's DropShadowEffect and provides Direction and ShadowDepth properties instead of OffsetX/OffsetY +/// +public class DropShadowDirectionEffect : DropShadowEffectBase, IDirectionDropShadowEffect, IMutableEffect +{ + public static readonly StyledProperty ShadowDepthProperty = + AvaloniaProperty.Register( + nameof(ShadowDepth), 5); + + public double ShadowDepth + { + get => GetValue(ShadowDepthProperty); + set => SetValue(ShadowDepthProperty, value); + } + + public static readonly StyledProperty DirectionProperty = AvaloniaProperty.Register( + nameof(Direction), 315); + + public double Direction + { + get => GetValue(DirectionProperty); + set => SetValue(DirectionProperty, value); + } + + public double OffsetX => Math.Cos(Direction * Math.PI / 180) * ShadowDepth; + public double OffsetY => Math.Sin(Direction * Math.PI / 180) * ShadowDepth; + + public IImmutableEffect ToImmutable() => new ImmutableDropShadowDirectionEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/Effect.cs b/src/Avalonia.Base/Media/Effects/Effect.cs new file mode 100644 index 0000000000..182e8613f8 --- /dev/null +++ b/src/Avalonia.Base/Media/Effects/Effect.cs @@ -0,0 +1,93 @@ +using System; +using Avalonia.Animation; +using Avalonia.Animation.Animators; +using Avalonia.Reactive; +using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Utilities; + +// ReSharper disable once CheckNamespace +namespace Avalonia.Media; + +public class Effect : Animatable, IAffectsRender +{ + /// + /// Marks a property as affecting the brush's visual representation. + /// + /// The properties. + /// + /// After a call to this method in a brush's static constructor, any change to the + /// property will cause the event to be raised on the brush. + /// + protected static void AffectsRender(params AvaloniaProperty[] properties) + where T : Effect + { + var invalidateObserver = new AnonymousObserver( + static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty)); + + foreach (var property in properties) + { + property.Changed.Subscribe(invalidateObserver); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + + /// + public event EventHandler? Invalidated; + + + static Exception ParseError(string s) => throw new ArgumentException("Unable to parse effect: " + s); + public static IEffect Parse(string s) + { + var span = s.AsSpan(); + var r = new TokenParser(span); + if (r.TryConsume("blur")) + { + if (!r.TryConsume('(') || !r.TryParseDouble(out var radius) || !r.TryConsume(')') || !r.IsEofWithWhitespace()) + throw ParseError(s); + return new ImmutableBlurEffect(radius); + } + + + if (r.TryConsume("drop-shadow")) + { + if (!r.TryConsume('(') || !r.TryParseDouble(out var offsetX) + || !r.TryParseDouble(out var offsetY)) + throw ParseError(s); + double blurRadius = 0; + var color = Colors.Black; + if (!r.TryConsume(')')) + { + if (!r.TryParseDouble(out blurRadius) || blurRadius < 0) + throw ParseError(s); + if (!r.TryConsume(')')) + { + var endOfExpression = s.LastIndexOf(")", StringComparison.Ordinal); + if (endOfExpression == -1) + throw ParseError(s); + + if (!new TokenParser(span.Slice(endOfExpression + 1)).IsEofWithWhitespace()) + throw ParseError(s); + + if (!Color.TryParse(span.Slice(r.Position, endOfExpression - r.Position).TrimEnd(), out color)) + throw ParseError(s); + return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1); + } + } + if (!r.IsEofWithWhitespace()) + throw ParseError(s); + return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1); + } + + throw ParseError(s); + } + + static Effect() + { + EffectAnimator.EnsureRegistered(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/EffectAnimator.cs b/src/Avalonia.Base/Media/Effects/EffectAnimator.cs new file mode 100644 index 0000000000..70d359911b --- /dev/null +++ b/src/Avalonia.Base/Media/Effects/EffectAnimator.cs @@ -0,0 +1,131 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; +using Avalonia.Logging; +using Avalonia.Media; + +// ReSharper disable once CheckNamespace +namespace Avalonia.Animation.Animators; + +public class EffectAnimator : Animator +{ + public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, + IObservable match, Action? onComplete) + { + if (TryCreateAnimator(out var animator) + || TryCreateAnimator(out animator)) + return animator.Apply(animation, control, clock, match, onComplete); + + Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log( + this, + "The animation's keyframe value types set is not supported."); + + return base.Apply(animation, control, clock, match, onComplete); + } + + private bool TryCreateAnimator([NotNullWhen(true)] out IAnimator? animator) + where TAnimator : EffectAnimatorBase, new() where TInterface : class, IEffect + { + TAnimator? createdAnimator = null; + foreach (var keyFrame in this) + { + if (keyFrame.Value is TInterface) + { + createdAnimator ??= new TAnimator() + { + Property = Property + }; + createdAnimator.Add(new AnimatorKeyFrame(typeof(TAnimator), () => new TAnimator(), keyFrame.Cue, + keyFrame.KeySpline) + { + Value = keyFrame.Value + }); + } + else + { + animator = null; + return false; + } + } + + animator = createdAnimator; + return animator != null; + } + + /// + /// Fallback implementation of animation. + /// + public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue) => progress >= 0.5 ? newValue : oldValue; + + private static bool s_Registered; + public static void EnsureRegistered() + { + if(s_Registered) + return; + s_Registered = true; + Animation.RegisterAnimator(prop => + typeof(IEffect).IsAssignableFrom(prop.PropertyType)); + } +} + +public abstract class EffectAnimatorBase : Animator where T : class, IEffect? +{ + public override IDisposable BindAnimation(Animatable control, IObservable instance) + { + if (Property is null) + { + throw new InvalidOperationException("Animator has no property specified."); + } + + return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); + } + + protected abstract T Interpolate(double progress, T oldValue, T newValue); + public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue) + { + var old = oldValue as T; + var n = newValue as T; + if (old == null || n == null) + return progress >= 0.5 ? newValue : oldValue; + return Interpolate(progress, old, n); + } +} + +public class BlurEffectAnimator : EffectAnimatorBase +{ + private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator(); + + protected override IBlurEffect Interpolate(double progress, IBlurEffect oldValue, IBlurEffect newValue) + { + return new ImmutableBlurEffect( + s_doubleAnimator.Interpolate(progress, oldValue.Radius, newValue.Radius)); + } +} + +public class DropShadowEffectAnimator : EffectAnimatorBase +{ + private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator(); + + protected override IDropShadowEffect Interpolate(double progress, IDropShadowEffect oldValue, + IDropShadowEffect newValue) + { + var blur = s_doubleAnimator.Interpolate(progress, oldValue.BlurRadius, newValue.BlurRadius); + var color = ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color); + var opacity = s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity); + + if (oldValue is IDirectionDropShadowEffect oldDirection && newValue is IDirectionDropShadowEffect newDirection) + { + return new ImmutableDropShadowDirectionEffect( + s_doubleAnimator.Interpolate(progress, oldDirection.Direction, newDirection.Direction), + s_doubleAnimator.Interpolate(progress, oldDirection.ShadowDepth, newDirection.ShadowDepth), + blur, color, opacity + ); + } + + return new ImmutableDropShadowEffect( + s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX), + s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY), + blur, color, opacity + ); + } +} diff --git a/src/Avalonia.Base/Media/Effects/EffectConverter.cs b/src/Avalonia.Base/Media/Effects/EffectConverter.cs new file mode 100644 index 0000000000..6ec3bace03 --- /dev/null +++ b/src/Avalonia.Base/Media/Effects/EffectConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Avalonia.Media; + +public class EffectConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value) + { + return value is string s ? Effect.Parse(s) : null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/EffectExtesions.cs b/src/Avalonia.Base/Media/Effects/EffectExtesions.cs new file mode 100644 index 0000000000..adc287607b --- /dev/null +++ b/src/Avalonia.Base/Media/Effects/EffectExtesions.cs @@ -0,0 +1,56 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace Avalonia.Media; + +public static class EffectExtensions +{ + static double AdjustPaddingRadius(double radius) + { + if (radius <= 0) + return 0; + return Math.Ceiling(radius) + 1; + } + internal static Thickness GetEffectOutputPadding(this IEffect? effect) + { + if (effect == null) + return default; + if (effect is IBlurEffect blur) + return new Thickness(AdjustPaddingRadius(blur.Radius)); + if (effect is IDropShadowEffect dropShadowEffect) + { + var radius = AdjustPaddingRadius(dropShadowEffect.BlurRadius); + var rc = new Rect(-radius, -radius, + radius * 2, radius * 2); + rc = rc.Translate(new(dropShadowEffect.OffsetX, dropShadowEffect.OffsetY)); + return new Thickness(Math.Max(0, 0 - rc.X), + Math.Max(0, 0 - rc.Y), Math.Max(0, rc.Right), Math.Max(0, rc.Bottom)); + } + + throw new ArgumentException("Unknown effect type: " + effect.GetType()); + } + + /// + /// Converts a effect to an immutable effect. + /// + /// The effect. + /// + /// The result of calling if the effect is mutable, + /// otherwise . + /// + public static IImmutableEffect ToImmutable(this IEffect effect) + { + _ = effect ?? throw new ArgumentNullException(nameof(effect)); + + return (effect as IMutableEffect)?.ToImmutable() ?? (IImmutableEffect)effect; + } + + internal static bool EffectEquals(this IImmutableEffect? immutable, IEffect? right) + { + if (immutable == null && right == null) + return true; + if (immutable != null && right != null) + return immutable.Equals(right); + return false; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/EffectTransition.cs b/src/Avalonia.Base/Media/Effects/EffectTransition.cs new file mode 100644 index 0000000000..b2e0d07355 --- /dev/null +++ b/src/Avalonia.Base/Media/Effects/EffectTransition.cs @@ -0,0 +1,83 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Animation.Animators; +using Avalonia.Animation.Easings; +using Avalonia.Media; + + +// ReSharper disable once CheckNamespace +namespace Avalonia.Animation; + +/// +/// Transition class that handles with type. +/// +public class EffectTransition : Transition +{ + private static readonly BlurEffectAnimator s_blurEffectAnimator = new(); + private static readonly DropShadowEffectAnimator s_dropShadowEffectAnimator = new(); + private static readonly ImmutableBlurEffect s_DefaultBlur = new ImmutableBlurEffect(0); + private static readonly ImmutableDropShadowDirectionEffect s_DefaultDropShadow = new(0, 0, 0, default, 0); + + bool TryWithAnimator( + IObservable progress, + TAnimator animator, + IEffect? oldValue, IEffect? newValue, TInterface defaultValue, [MaybeNullWhen(false)] out IObservable observable) + where TAnimator : EffectAnimatorBase where TInterface : class, IEffect + { + observable = null; + TInterface? oldI = null, newI = null; + if (oldValue is TInterface oi) + { + oldI = oi; + if (newValue is TInterface ni) + newI = ni; + else if (newValue == null) + newI = defaultValue; + else + return false; + } + else if (newValue is TInterface nv) + { + oldI = defaultValue; + newI = nv; + + } + else + return false; + + observable = new AnimatorTransitionObservable>(animator, progress, Easing, oldI, newI); + return true; + + } + + public override IObservable DoTransition(IObservable progress, IEffect? oldValue, IEffect? newValue) + { + if ((oldValue != null || newValue != null) + && ( + TryWithAnimator(progress, s_blurEffectAnimator, + oldValue, newValue, s_DefaultBlur, out var observable) + || TryWithAnimator(progress, s_dropShadowEffectAnimator, + oldValue, newValue, s_DefaultDropShadow, out observable) + )) + return observable; + + return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); + } + + private sealed class IncompatibleTransitionObservable : TransitionObservableBase + { + private readonly IEffect? _from; + private readonly IEffect? _to; + + public IncompatibleTransitionObservable(IObservable progress, Easing easing, IEffect? from, IEffect? to) : base(progress, easing) + { + _from = from; + _to = to; + } + + protected override IEffect? ProduceValue(double progress) + { + return progress >= 0.5 ? _to : _from; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/IBlurEffect.cs b/src/Avalonia.Base/Media/Effects/IBlurEffect.cs new file mode 100644 index 0000000000..716159747c --- /dev/null +++ b/src/Avalonia.Base/Media/Effects/IBlurEffect.cs @@ -0,0 +1,29 @@ +// ReSharper disable once CheckNamespace + +using Avalonia.Animation.Animators; + +namespace Avalonia.Media; + +public interface IBlurEffect : IEffect +{ + double Radius { get; } +} + +public class ImmutableBlurEffect : IBlurEffect, IImmutableEffect +{ + static ImmutableBlurEffect() + { + EffectAnimator.EnsureRegistered(); + } + + public ImmutableBlurEffect(double radius) + { + Radius = radius; + } + + public double Radius { get; } + + public bool Equals(IEffect? other) => + // ReSharper disable once CompareOfFloatsByEqualityOperator + other is IBlurEffect blur && blur.Radius == Radius; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs b/src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs new file mode 100644 index 0000000000..bb97410d7f --- /dev/null +++ b/src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs @@ -0,0 +1,84 @@ +// ReSharper disable once CheckNamespace + +using System; +using Avalonia.Animation.Animators; + +namespace Avalonia.Media; + +public interface IDropShadowEffect : IEffect +{ + double OffsetX { get; } + double OffsetY { get; } + double BlurRadius { get; } + Color Color { get; } + double Opacity { get; } +} + +internal interface IDirectionDropShadowEffect : IDropShadowEffect +{ + double Direction { get; } + double ShadowDepth { get; } +} + +public class ImmutableDropShadowEffect : IDropShadowEffect, IImmutableEffect +{ + static ImmutableDropShadowEffect() + { + EffectAnimator.EnsureRegistered(); + } + + public ImmutableDropShadowEffect(double offsetX, double offsetY, double blurRadius, Color color, double opacity) + { + OffsetX = offsetX; + OffsetY = offsetY; + BlurRadius = blurRadius; + Color = color; + Opacity = opacity; + } + + public double OffsetX { get; } + public double OffsetY { get; } + public double BlurRadius { get; } + public Color Color { get; } + public double Opacity { get; } + public bool Equals(IEffect? other) + { + return other is IDropShadowEffect d + && d.OffsetX == OffsetX && d.OffsetY == OffsetY + && d.BlurRadius == BlurRadius + && d.Color == Color && d.Opacity == Opacity; + } +} + + +public class ImmutableDropShadowDirectionEffect : IDirectionDropShadowEffect, IImmutableEffect +{ + static ImmutableDropShadowDirectionEffect() + { + EffectAnimator.EnsureRegistered(); + } + + public ImmutableDropShadowDirectionEffect(double direction, double shadowDepth, double blurRadius, Color color, double opacity) + { + Direction = direction; + ShadowDepth = shadowDepth; + BlurRadius = blurRadius; + Color = color; + Opacity = opacity; + } + + public double OffsetX => Math.Cos(Direction * Math.PI / 180) * ShadowDepth; + public double OffsetY => Math.Sin(Direction * Math.PI / 180) * ShadowDepth; + public double Direction { get; } + public double ShadowDepth { get; } + public double BlurRadius { get; } + public Color Color { get; } + public double Opacity { get; } + public bool Equals(IEffect? other) + { + return other is IDropShadowEffect d + && d.OffsetX == OffsetX && d.OffsetY == OffsetY + && d.BlurRadius == BlurRadius + && d.Color == Color && d.Opacity == Opacity; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/IEffect.cs b/src/Avalonia.Base/Media/Effects/IEffect.cs new file mode 100644 index 0000000000..698dccf1dd --- /dev/null +++ b/src/Avalonia.Base/Media/Effects/IEffect.cs @@ -0,0 +1,26 @@ +// ReSharper disable once CheckNamespace + +using System; +using System.ComponentModel; + +namespace Avalonia.Media; + +[TypeConverter(typeof(EffectConverter))] +public interface IEffect +{ + +} + +public interface IMutableEffect : IEffect, IAffectsRender +{ + /// + /// Creates an immutable clone of the effect. + /// + /// The immutable clone. + internal IImmutableEffect ToImmutable(); +} + +public interface IImmutableEffect : IEffect, IEquatable +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 2e8d8e415d..4425147098 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -30,13 +30,15 @@ namespace Avalonia.Media _fontFallbacks = options?.FontFallbacks; - DefaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName(); + var defaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName(); - if (string.IsNullOrEmpty(DefaultFontFamilyName)) + if (string.IsNullOrEmpty(defaultFontFamilyName)) { throw new InvalidOperationException("Default font family name can't be null or empty."); } + DefaultFontFamily = new FontFamily(defaultFontFamilyName); + AddFontCollection(new SystemFontCollection(this)); } @@ -65,9 +67,9 @@ namespace Avalonia.Media } /// - /// Gets the system's default font family's name. + /// Gets the system's default font family. /// - public string DefaultFontFamilyName + public FontFamily DefaultFontFamily { get; } @@ -93,6 +95,11 @@ namespace Avalonia.Media var fontFamily = typeface.FontFamily; + if(typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName) + { + return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); + } + if (fontFamily.Key is FontFamilyKey key) { var source = key.Source; @@ -131,15 +138,21 @@ namespace Avalonia.Media } } - foreach (var familyName in fontFamily.FamilyNames) + for (var i = 0; i < fontFamily.FamilyNames.Count; i++) { + var familyName = fontFamily.FamilyNames[i]; + if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { - return true; + if (!fontFamily.FamilyNames.HasFallbacks || glyphTypeface.FamilyName != DefaultFontFamily.Name) + { + return true; + } } } - return TryGetGlyphTypeface(new Typeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); + //Nothing was found so use the default + return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); } /// @@ -199,16 +212,37 @@ namespace Avalonia.Media { foreach (var fallback in _fontFallbacks) { - typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); + if (fallback.UnicodeRange.IsInRange(codepoint)) + { + typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); + + if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { + return true; + } + } + } + } - if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + //Try to match against fallbacks first + if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks) + { + for (int i = 1; i < fontFamily.FamilyNames.Count; i++) + { + var familyName = fontFamily.FamilyNames[i]; + + foreach (var fontCollection in _fontCollections.Values) { - return true; + if (fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface)) + { + return true; + }; } } } - return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, fontFamily, culture, out typeface); + //Try to find a match with the system font manager + return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface); } } } diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index f2fb490592..4d4751db02 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -8,10 +8,8 @@ using Avalonia.Platform; namespace Avalonia.Media.Fonts { - public class EmbeddedFontCollection : IFontCollection + public class EmbeddedFontCollection : FontCollectionBase { - private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); - private readonly List _fontFamilies = new List(1); private readonly Uri _key; @@ -25,13 +23,13 @@ namespace Avalonia.Media.Fonts _source = source; } - public Uri Key => _key; + public override Uri Key => _key; - public FontFamily this[int index] => _fontFamilies[index]; + public override FontFamily this[int index] => _fontFamilies[index]; - public int Count => _fontFamilies.Count; + public override int Count => _fontFamilies.Count; - public void Initialize(IFontManagerImpl fontManager) + public override void Initialize(IFontManagerImpl fontManager) { var assetLoader = AvaloniaLocator.Current.GetRequiredService(); @@ -45,7 +43,7 @@ namespace Avalonia.Media.Fonts { if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) { - glyphTypefaces = new ConcurrentDictionary(); + glyphTypefaces = new ConcurrentDictionary(); if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces)) { @@ -63,27 +61,8 @@ namespace Avalonia.Media.Fonts } } - public void Dispose() - { - foreach (var fontFamily in _fontFamilies) - { - if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces)) - { - foreach (var glyphTypeface in glyphTypefaces.Values) - { - glyphTypeface.Dispose(); - } - } - } - GC.SuppressFinalize(this); - } - - public IEnumerator GetEnumerator() => _fontFamilies.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { var key = new FontCollectionKey(style, weight, stretch); @@ -116,175 +95,6 @@ namespace Avalonia.Media.Fonts return false; } - private static bool TryGetNearestMatch( - ConcurrentDictionary glyphTypefaces, - FontCollectionKey key, - [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) - { - if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) - { - return true; - } - - if (key.Style != FontStyle.Normal) - { - key = key with { Style = FontStyle.Normal }; - } - - if (key.Stretch != FontStretch.Normal) - { - if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) - { - return true; - } - - if (key.Weight != FontWeight.Normal) - { - if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface)) - { - return true; - } - } - - key = key with { Stretch = FontStretch.Normal }; - } - - if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface)) - { - return true; - } - - if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) - { - return true; - } - - //Take the first glyph typeface we can find. - foreach (var typeface in glyphTypefaces.Values) - { - glyphTypeface = typeface; - - return true; - } - - return false; - } - - private static bool TryFindStretchFallback( - ConcurrentDictionary glyphTypefaces, - FontCollectionKey key, - [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) - { - glyphTypeface = null; - - var stretch = (int)key.Stretch; - - if (stretch < 5) - { - for (var i = 0; stretch + i < 9; i++) - { - if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface)) - { - return true; - } - } - } - else - { - for (var i = 0; stretch - i > 1; i++) - { - if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface)) - { - return true; - } - } - } - - return false; - } - - private static bool TryFindWeightFallback( - ConcurrentDictionary glyphTypefaces, - FontCollectionKey key, - [NotNullWhen(true)] out IGlyphTypeface? typeface) - { - typeface = null; - var weight = (int)key.Weight; - - //If the target weight given is between 400 and 500 inclusive - if (weight >= 400 && weight <= 500) - { - //Look for available weights between the target and 500, in ascending order. - for (var i = 0; weight + i <= 500; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight - i >= 100; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights greater than 500, in ascending order. - for (var i = 0; weight + i <= 900; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) - { - return true; - } - } - } - - //If a weight less than 400 is given, look for available weights less than the target, in descending order. - if (weight < 400) - { - for (var i = 0; weight - i >= 100; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight + i <= 900; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) - { - return true; - } - } - } - - //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. - if (weight > 500) - { - for (var i = 0; weight + i <= 900; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface)) - { - return true; - } - } - - //If no match is found, look for available weights less than the target, in descending order. - for (var i = 0; weight - i >= 100; i += 50) - { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface)) - { - return true; - } - } - } - - return false; - } + public override IEnumerator GetEnumerator() => _fontFamilies.GetEnumerator(); } } diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs new file mode 100644 index 0000000000..713b3dafcd --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + public abstract class FontCollectionBase : IFontCollection + { + protected readonly ConcurrentDictionary> _glyphTypefaceCache = new(); + + public abstract Uri Key { get; } + + public abstract int Count { get; } + + public abstract FontFamily this[int index] { get; } + + public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + + public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, + string? familyName, CultureInfo? culture, out Typeface match) + { + match = default; + + if (string.IsNullOrEmpty(familyName)) + { + foreach (var typefaces in _glyphTypefaceCache.Values) + { + if (TryGetNearestMatch(typefaces, new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch }, out var glyphTypeface)) + { + if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { + match = new Typeface(glyphTypeface.FamilyName, style, weight, stretch); + + return true; + } + } + } + } + else + { + if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface)) + { + if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + { + match = new Typeface(familyName, style, weight, stretch); + + return true; + } + } + } + + return false; + } + + public abstract void Initialize(IFontManagerImpl fontManager); + + public abstract IEnumerator GetEnumerator(); + + void IDisposable.Dispose() + { + foreach (var glyphTypefaces in _glyphTypefaceCache.Values) + { + foreach (var pair in glyphTypefaces) + { + pair.Value?.Dispose(); + } + } + + GC.SuppressFinalize(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + internal static bool TryGetNearestMatch( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + + if (key.Style != FontStyle.Normal) + { + key = key with { Style = FontStyle.Normal }; + } + + if (key.Stretch != FontStretch.Normal) + { + if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + if (key.Weight != FontWeight.Normal) + { + if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface)) + { + return true; + } + } + + key = key with { Stretch = FontStretch.Normal }; + } + + if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + { + return true; + } + + //Take the first glyph typeface we can find. + foreach (var typeface in glyphTypefaces.Values) + { + if(typeface != null) + { + glyphTypeface = typeface; + + return true; + } + } + + return false; + } + + internal static bool TryFindStretchFallback( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + glyphTypeface = null; + + var stretch = (int)key.Stretch; + + if (stretch < 5) + { + for (var i = 0; stretch + i < 9; i++) + { + if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + else + { + for (var i = 0; stretch - i > 1; i++) + { + if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + + return false; + } + + internal static bool TryFindWeightFallback( + ConcurrentDictionary glyphTypefaces, + FontCollectionKey key, + [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) + { + glyphTypeface = null; + var weight = (int)key.Weight; + + //If the target weight given is between 400 and 500 inclusive + if (weight >= 400 && weight <= 500) + { + //Look for available weights between the target and 500, in ascending order. + for (var i = 0; weight + i <= 500; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + + //If no match is found, look for available weights greater than 500, in ascending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + + //If a weight less than 400 is given, look for available weights less than the target, in descending order. + if (weight < 400) + { + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + + //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order. + if (weight > 500) + { + for (var i = 0; weight + i <= 900; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + + //If no match is found, look for available weights less than the target, in descending order. + for (var i = 0; weight - i >= 100; i += 50) + { + if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + { + return true; + } + } + } + + return false; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs index 814230bcf3..1a30f168f1 100644 --- a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Avalonia.Platform; namespace Avalonia.Media.Fonts @@ -29,5 +30,21 @@ namespace Avalonia.Media.Fonts /// Returns true if a glyph typface can be found; otherwise, false bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface); + + /// + /// Tries to match a specified character to a that supports specified font properties. + /// + /// The codepoint to match against. + /// The font style. + /// The font weight. + /// The font stretch. + /// The family name. This is optional and used for fallback lookup. + /// The culture. + /// The matching . + /// + /// True, if the could match the character to specified parameters, False otherwise. + /// + bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, + FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface); } } diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index fd332c6ebe..2f2948cb3e 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -7,10 +6,8 @@ using Avalonia.Platform; namespace Avalonia.Media.Fonts { - internal class SystemFontCollection : IFontCollection + internal class SystemFontCollection : FontCollectionBase { - private readonly ConcurrentDictionary> _glyphTypefaceCache = new(); - private readonly FontManager _fontManager; private readonly string[] _familyNames; @@ -20,9 +17,9 @@ namespace Avalonia.Media.Fonts _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames(); } - public Uri Key => FontManager.SystemFontsKey; + public override Uri Key => FontManager.SystemFontsKey; - public FontFamily this[int index] + public override FontFamily this[int index] { get { @@ -32,76 +29,41 @@ namespace Avalonia.Media.Fonts } } - public int Count => _familyNames.Length; + public override int Count => _familyNames.Length; - public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, + public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { - if (familyName == FontFamily.DefaultFontFamilyName) - { - familyName = _fontManager.DefaultFontFamilyName; - } + glyphTypeface = null; var key = new FontCollectionKey(style, weight, stretch); - if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) - { - if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) - { - return true; - } - else - { - if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) && - glyphTypefaces.TryAdd(key, glyphTypeface)) - { - return true; - } - } - } + var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName, (key) => new ConcurrentDictionary()); - if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface)) + if (!glyphTypefaces.TryGetValue(key, out glyphTypeface)) { - glyphTypefaces = new ConcurrentDictionary(); + _fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface); - if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces)) + if (!glyphTypefaces.TryAdd(key, glyphTypeface)) { - return true; + return false; } } - return false; + return glyphTypeface != null; } - public void Initialize(IFontManagerImpl fontManager) + public override void Initialize(IFontManagerImpl fontManager) { //We initialize the system font collection during construction. } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public IEnumerator GetEnumerator() + public override IEnumerator GetEnumerator() { foreach (var familyName in _familyNames) { yield return new FontFamily(familyName); } } - - void IDisposable.Dispose() - { - foreach (var glyphTypefaces in _glyphTypefaceCache.Values) - { - foreach (var pair in glyphTypefaces) - { - pair.Value.Dispose(); - } - } - - GC.SuppressFinalize(this); - } } } diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index abfd2e33ac..75d7e44ab8 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media public class GeometryDrawing : Drawing { // Adding the Pen's stroke thickness here could yield wrong results due to transforms. - private static readonly IPen s_boundsPen = new ImmutablePen(Colors.Black.ToUint32(), 0); + private static readonly IPen s_boundsPen = new ImmutablePen(Colors.Black.ToUInt32(), 0); /// /// Defines the property. diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index d795cca894..20e0f96ff7 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -153,7 +153,7 @@ namespace Avalonia.Media /// /// Gets the conservative bounding box of the . /// - public Rect Bounds => PlatformImpl.Item.Bounds; + public Rect Bounds => new Rect(new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height)); /// /// @@ -166,7 +166,7 @@ namespace Avalonia.Media /// public Point BaselineOrigin { - get => PlatformImpl.Item.BaselineOrigin; + get => _baselineOrigin ?? new Point(0, Metrics.Baseline); set => Set(ref _baselineOrigin, value); } @@ -676,13 +676,17 @@ namespace Avalonia.Media } } - return new GlyphRunMetrics( - width, - trailingWhitespaceLength, - newLineLength, - firstCluster, - lastCluster - ); + return new GlyphRunMetrics + { + Baseline = -GlyphTypeface.Metrics.Ascent * Scale, + Width = width, + WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace, + Height = height, + NewLineLength = newLineLength, + TrailingWhitespaceLength = trailingWhitespaceLength, + FirstCluster = firstCluster, + LastCluster = lastCluster + }; } private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount) @@ -820,10 +824,11 @@ namespace Avalonia.Media private IRef CreateGlyphRunImpl() { var platformImpl = s_renderInterface.CreateGlyphRun( - GlyphTypeface, - FontRenderingEmSize, - GlyphInfos, - _baselineOrigin ?? new Point(0, -GlyphTypeface.Metrics.Ascent * Scale)); + GlyphTypeface, + FontRenderingEmSize, + GlyphInfos, + BaselineOrigin, + Bounds); _platformImpl = RefCountable.Create(platformImpl); @@ -835,5 +840,16 @@ namespace Avalonia.Media _platformImpl?.Dispose(); _platformImpl = null; } + + /// + /// Gets the intersections of specified upper and lower limit. + /// + /// Upper limit. + /// Lower limit. + /// + public IReadOnlyList GetIntersections(float lowerLimit, float upperLimit) + { + return PlatformImpl.Item.GetIntersections(lowerLimit, upperLimit); + } } } diff --git a/src/Avalonia.Base/Media/GlyphRunMetrics.cs b/src/Avalonia.Base/Media/GlyphRunMetrics.cs index 09b183d044..9ca1d5ea12 100644 --- a/src/Avalonia.Base/Media/GlyphRunMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphRunMetrics.cs @@ -2,23 +2,20 @@ { public readonly record struct GlyphRunMetrics { - public GlyphRunMetrics(double width, int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster) - { - Width = width; - TrailingWhitespaceLength = trailingWhitespaceLength; - NewLineLength = newLineLength; - FirstCluster = firstCluster; - LastCluster = lastCluster; - } + public double Baseline { get; init; } - public double Width { get; } + public double Width { get; init; } - public int TrailingWhitespaceLength { get; } + public double WidthIncludingTrailingWhitespace { get; init; } - public int NewLineLength { get; } + public double Height { get; init; } - public int FirstCluster { get; } + public int TrailingWhitespaceLength { get; init; } - public int LastCluster { get; } + public int NewLineLength { get; init; } + + public int FirstCluster { get; init; } + + public int LastCluster { get; init; } } } diff --git a/src/Avalonia.Base/Media/GradientBrush.cs b/src/Avalonia.Base/Media/GradientBrush.cs index e1654a01b2..971d4fdd58 100644 --- a/src/Avalonia.Base/Media/GradientBrush.cs +++ b/src/Avalonia.Base/Media/GradientBrush.cs @@ -38,6 +38,8 @@ namespace Avalonia.Media /// /// Initializes a new instance of the class. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", + Justification = "Collection properties shouldn't be set with SetCurrentValue.")] public GradientBrush() { this.GradientStops = new GradientStops(); diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs index b4bf6fd217..897c883875 100644 --- a/src/Avalonia.Base/Media/HslColor.cs +++ b/src/Avalonia.Base/Media/HslColor.cs @@ -98,43 +98,13 @@ namespace Avalonia.Media L = hsl.L; } - /// - /// Gets the Alpha (transparency) component in the range from 0..1 (percentage). - /// - /// - /// - /// 0 is fully transparent. - /// 1 is fully opaque. - /// - /// + /// public double A { get; } - /// - /// Gets the Hue component in the range from 0..360 (degrees). - /// This is the color's location, in degrees, on a color wheel/circle from 0 to 360. - /// Note that 360 is equivalent to 0 and will be adjusted automatically. - /// - /// - /// - /// 0/360 degrees is Red. - /// 60 degrees is Yellow. - /// 120 degrees is Green. - /// 180 degrees is Cyan. - /// 240 degrees is Blue. - /// 300 degrees is Magenta. - /// - /// + /// public double H { get; } - /// - /// Gets the Saturation component in the range from 0..1 (percentage). - /// - /// - /// - /// 0 is a shade of gray (no color). - /// 1 is the full color. - /// - /// + /// public double S { get; } /// diff --git a/src/Avalonia.Base/Media/IImage.cs b/src/Avalonia.Base/Media/IImage.cs index cbe25b7b58..4e0b952b88 100644 --- a/src/Avalonia.Base/Media/IImage.cs +++ b/src/Avalonia.Base/Media/IImage.cs @@ -18,11 +18,9 @@ namespace Avalonia.Media /// The drawing context. /// The rect in the image to draw. /// The rect in the output to draw to. - /// The bitmap interpolation mode. void Draw( DrawingContext context, Rect sourceRect, - Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode); + Rect destRect); } } diff --git a/src/Avalonia.Base/Media/ITileBrush.cs b/src/Avalonia.Base/Media/ITileBrush.cs index cb5a591003..586f6053a1 100644 --- a/src/Avalonia.Base/Media/ITileBrush.cs +++ b/src/Avalonia.Base/Media/ITileBrush.cs @@ -39,13 +39,5 @@ namespace Avalonia.Media /// Gets the brush's tile mode. /// TileMode TileMode { get; } - - /// - /// Gets the bitmap interpolation mode. - /// - /// - /// The bitmap interpolation mode. - /// - BitmapInterpolationMode BitmapInterpolationMode { get; } } } diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index c4720d772e..07bb3db100 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -224,15 +224,13 @@ namespace Avalonia.Media.Imaging void IImage.Draw( DrawingContext context, Rect sourceRect, - Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode) + Rect destRect) { context.DrawBitmap( PlatformImpl, 1, sourceRect, - destRect, - bitmapInterpolationMode); + destRect); } private static IPlatformRenderInterface GetFactory() diff --git a/src/Avalonia.Base/Media/Imaging/BitmapBlendingMode.cs b/src/Avalonia.Base/Media/Imaging/BitmapBlendingMode.cs index eb39020939..73a3f7b269 100644 --- a/src/Avalonia.Base/Media/Imaging/BitmapBlendingMode.cs +++ b/src/Avalonia.Base/Media/Imaging/BitmapBlendingMode.cs @@ -3,8 +3,10 @@ namespace Avalonia.Media.Imaging /// /// Controls the way the bitmaps are drawn together. /// - public enum BitmapBlendingMode + public enum BitmapBlendingMode : byte { + Unspecified, + /// /// Source is placed over the destination. /// @@ -52,6 +54,6 @@ namespace Avalonia.Media.Imaging /// /// Display the sum of the source image and destination image. /// - Plus, + Plus } } diff --git a/src/Avalonia.Base/Media/Imaging/BitmapInterpolationMode.cs b/src/Avalonia.Base/Media/Imaging/BitmapInterpolationMode.cs index 7cdb5d8b9f..eaa64892a4 100644 --- a/src/Avalonia.Base/Media/Imaging/BitmapInterpolationMode.cs +++ b/src/Avalonia.Base/Media/Imaging/BitmapInterpolationMode.cs @@ -3,12 +3,14 @@ /// /// Controls the performance and quality of bitmap scaling. /// - public enum BitmapInterpolationMode + public enum BitmapInterpolationMode : byte { + Unspecified, + /// - /// Uses the default behavior of the underling render backend. + /// Disable interpolation. /// - Default, + None, /// /// The best performance but worst image quality. @@ -18,7 +20,7 @@ /// /// Good performance and decent image quality. /// - MediumQuality, + MediumQuality, /// /// Highest quality but worst performance. diff --git a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs index 8cdf5b592a..93556679e9 100644 --- a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs @@ -83,12 +83,12 @@ namespace Avalonia.Media.Imaging } } - public void Draw(DrawingContext context, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public void Draw(DrawingContext context, Rect sourceRect, Rect destRect) { if (Source is not IBitmap bmp) return; var topLeft = SourceRect.TopLeft.ToPointWithDpi(bmp.Dpi); - Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode); + Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect); } } } diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index e77dd9d1ab..4921e9b756 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.Imaging /// /// A bitmap that holds the rendering of a . /// - public class RenderTargetBitmap : Bitmap, IDisposable + public class RenderTargetBitmap : Bitmap { /// /// Initializes a new instance of the class. @@ -68,5 +68,11 @@ namespace Avalonia.Media.Imaging platform.Clear(Colors.Transparent); return new PlatformDrawingContext(platform); } + + public override void Dispose() + { + PlatformImpl.Dispose(); + base.Dispose(); + } } } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 58b153482d..fdf10596bb 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Avalonia.Platform; -using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Media.Imaging; @@ -80,11 +79,10 @@ namespace Avalonia.Media /// The bitmap. /// The rect in the image to draw. /// The rect in the output to draw to. - /// The bitmap interpolation mode. - public void DrawBitmap(IBitmap source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) + public void DrawBitmap(IBitmap source, Rect sourceRect, Rect destRect) { _ = source ?? throw new ArgumentNullException(nameof(source)); - PlatformImpl.DrawBitmap(source.PlatformImpl, 1, sourceRect, destRect, bitmapInterpolationMode); + PlatformImpl.DrawBitmap(source.PlatformImpl, 1, sourceRect, destRect); } /// diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs index 668a907fdf..175038ba75 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs @@ -22,7 +22,6 @@ namespace Avalonia.Media.Immutable /// How the source rectangle will be stretched to fill the destination rect. /// /// The tile mode. - /// The bitmap interpolation mode. public ImmutableImageBrush( IBitmap? source, AlignmentX alignmentX = AlignmentX.Center, @@ -33,8 +32,7 @@ namespace Avalonia.Media.Immutable RelativePoint transformOrigin = default, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, - TileMode tileMode = TileMode.None, - BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + TileMode tileMode = TileMode.None) : base( alignmentX, alignmentY, @@ -44,8 +42,7 @@ namespace Avalonia.Media.Immutable transformOrigin, sourceRect ?? RelativeRect.Fill, stretch, - tileMode, - bitmapInterpolationMode) + tileMode) { Source = source; } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs index 1ee52365e0..7e139af516 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs @@ -21,7 +21,6 @@ namespace Avalonia.Media.Immutable /// How the source rectangle will be stretched to fill the destination rect. /// /// The tile mode. - /// The bitmap interpolation mode. protected ImmutableTileBrush( AlignmentX alignmentX, AlignmentY alignmentY, @@ -31,8 +30,7 @@ namespace Avalonia.Media.Immutable RelativePoint transformOrigin, RelativeRect sourceRect, Stretch stretch, - TileMode tileMode, - BitmapInterpolationMode bitmapInterpolationMode) + TileMode tileMode) { AlignmentX = alignmentX; AlignmentY = alignmentY; @@ -43,7 +41,6 @@ namespace Avalonia.Media.Immutable SourceRect = sourceRect; Stretch = stretch; TileMode = tileMode; - BitmapInterpolationMode = bitmapInterpolationMode; } /// @@ -60,8 +57,7 @@ namespace Avalonia.Media.Immutable source.TransformOrigin, source.SourceRect, source.Stretch, - source.TileMode, - source.BitmapInterpolationMode) + source.TileMode) { } @@ -95,8 +91,5 @@ namespace Avalonia.Media.Immutable /// public TileMode TileMode { get; } - - /// - public BitmapInterpolationMode BitmapInterpolationMode { get; } } } diff --git a/src/Avalonia.Base/Media/PlatformDrawingContext.cs b/src/Avalonia.Base/Media/PlatformDrawingContext.cs index eb8a93722c..09c0cd26ac 100644 --- a/src/Avalonia.Base/Media/PlatformDrawingContext.cs +++ b/src/Avalonia.Base/Media/PlatformDrawingContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Logging; using Avalonia.Media.Imaging; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -25,6 +26,12 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi _ownsImpl = ownsImpl; } + public RenderOptions RenderOptions + { + get => _impl.RenderOptions; + set => _impl.RenderOptions = value; + } + protected override void DrawLineCore(IPen pen, Point p1, Point p2) => _impl.DrawLine(pen, p1, p2); @@ -37,12 +44,22 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect); - internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) => - _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) => + _impl.DrawBitmap(source, opacity, sourceRect, destRect); - public override void Custom(ICustomDrawOperation custom) => - custom.Render(_impl); + public override void Custom(ICustomDrawOperation custom) + { + using var immediateDrawingContext = new ImmediateDrawingContext(_impl, false); + try + { + custom.Render(immediateDrawingContext); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual) + ?.Log(custom, $"Exception in {custom.GetType().Name}.{nameof(ICustomDrawOperation.Render)} {{0}}", e); + } + } public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { @@ -65,9 +82,6 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) => _impl.PushOpacityMask(mask, bounds); - protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) => - _impl.PushBitmapBlendMode(blendingMode); - protected override void PushTransformCore(Matrix matrix) { _transforms ??= TransformStackPool.Get(); @@ -84,8 +98,6 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi protected override void PopOpacityMaskCore() => _impl.PopOpacityMask(); - protected override void PopBitmapBlendModeCore() => _impl.PopBitmapBlendMode(); - protected override void PopTransformCore() => _impl.Transform = (_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop(); diff --git a/src/Avalonia.Base/Media/PolyLineSegment.cs b/src/Avalonia.Base/Media/PolyLineSegment.cs index 5c48c11e19..51bf13d7cb 100644 --- a/src/Avalonia.Base/Media/PolyLineSegment.cs +++ b/src/Avalonia.Base/Media/PolyLineSegment.cs @@ -10,8 +10,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty PointsProperty - = AvaloniaProperty.Register(nameof(Points)); + public static readonly StyledProperty> PointsProperty + = AvaloniaProperty.Register>(nameof(Points)); /// /// Gets or sets the points. @@ -19,7 +19,7 @@ namespace Avalonia.Media /// /// The points. /// - public Points Points + public IList Points { get => GetValue(PointsProperty); set => SetValue(PointsProperty, value); @@ -28,6 +28,8 @@ namespace Avalonia.Media /// /// Initializes a new instance of the class. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", + Justification = "Collection properties shouldn't be set with SetCurrentValue.")] public PolyLineSegment() { Points = new Points(); @@ -37,9 +39,9 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The points. - public PolyLineSegment(IEnumerable points) : this() + public PolyLineSegment(IEnumerable points) { - Points.AddRange(points); + Points = new Points(points); } protected internal override void ApplyTo(StreamGeometryContext ctx) diff --git a/src/Avalonia.Base/Media/PolylineGeometry.cs b/src/Avalonia.Base/Media/PolylineGeometry.cs index dd3c298b5b..b0229b6455 100644 --- a/src/Avalonia.Base/Media/PolylineGeometry.cs +++ b/src/Avalonia.Base/Media/PolylineGeometry.cs @@ -14,8 +14,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly DirectProperty PointsProperty = - AvaloniaProperty.RegisterDirect(nameof(Points), g => g.Points, (g, f) => g.Points = f); + public static readonly DirectProperty> PointsProperty = + AvaloniaProperty.RegisterDirect>(nameof(Points), g => g.Points, (g, f) => g.Points = f); /// /// Defines the property. @@ -23,13 +23,13 @@ namespace Avalonia.Media public static readonly StyledProperty IsFilledProperty = AvaloniaProperty.Register(nameof(IsFilled)); - private Points _points; + private IList _points; private IDisposable? _pointsObserver; static PolylineGeometry() { AffectsGeometry(IsFilledProperty); - PointsProperty.Changed.AddClassHandler((s, e) => s.OnPointsChanged(e.NewValue as Points)); + PointsProperty.Changed.AddClassHandler((s, e) => s.OnPointsChanged(e.NewValue as IList)); } /// @@ -43,9 +43,9 @@ namespace Avalonia.Media /// /// Initializes a new instance of the class. /// - public PolylineGeometry(IEnumerable points, bool isFilled) : this() + public PolylineGeometry(IEnumerable points, bool isFilled) { - Points.AddRange(points); + _points = new Points(points); IsFilled = isFilled; } @@ -56,7 +56,7 @@ namespace Avalonia.Media /// The points. /// [Content] - public Points Points + public IList Points { get => _points; set => SetAndRaise(PointsProperty, ref _points, value); @@ -97,10 +97,10 @@ namespace Avalonia.Media return geometry; } - private void OnPointsChanged(Points? newValue) + private void OnPointsChanged(IList? newValue) { _pointsObserver?.Dispose(); - _pointsObserver = newValue?.ForEachItem( + _pointsObserver = (newValue as IAvaloniaList)?.ForEachItem( _ => InvalidateGeometry(), _ => InvalidateGeometry(), InvalidateGeometry); diff --git a/src/Avalonia.Base/Media/RenderOptions.cs b/src/Avalonia.Base/Media/RenderOptions.cs index 5863d0ac58..639498543b 100644 --- a/src/Avalonia.Base/Media/RenderOptions.cs +++ b/src/Avalonia.Base/Media/RenderOptions.cs @@ -1,36 +1,131 @@ using Avalonia.Media.Imaging; namespace Avalonia.Media -{ - public class RenderOptions +{ + public readonly record struct RenderOptions { + public BitmapInterpolationMode BitmapInterpolationMode { get; init; } + public EdgeMode EdgeMode { get; init; } + public TextRenderingMode TextRenderingMode { get; init; } + public BitmapBlendingMode BitmapBlendingMode { get; init; } + + /// + /// Gets the value of the BitmapInterpolationMode attached property for a visual. + /// + /// The control. + /// The control's left coordinate. + public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual visual) + { + return visual.RenderOptions.BitmapInterpolationMode; + } + /// - /// Defines the property. + /// Sets the value of the BitmapInterpolationMode attached property for a visual. /// - public static readonly StyledProperty BitmapInterpolationModeProperty = - AvaloniaProperty.RegisterAttached( - "BitmapInterpolationMode", - BitmapInterpolationMode.MediumQuality, - inherits: true); + /// The control. + /// The left value. + public static void SetBitmapInterpolationMode(Visual visual, BitmapInterpolationMode value) + { + visual.RenderOptions = visual.RenderOptions with { BitmapInterpolationMode = value }; + } /// - /// Gets the value of the BitmapInterpolationMode attached property for a control. + /// Gets the value of the BitmapBlendingMode attached property for a visual. /// - /// The control. + /// The control. /// The control's left coordinate. - public static BitmapInterpolationMode GetBitmapInterpolationMode(AvaloniaObject element) + public static BitmapBlendingMode GetBitmapBlendingMode(Visual visual) { - return element.GetValue(BitmapInterpolationModeProperty); + return visual.RenderOptions.BitmapBlendingMode; } /// - /// Sets the value of the BitmapInterpolationMode attached property for a control. + /// Sets the value of the BitmapBlendingMode attached property for a visual. /// - /// The control. + /// The control. /// The left value. - public static void SetBitmapInterpolationMode(AvaloniaObject element, BitmapInterpolationMode value) + public static void SetBitmapBlendingMode(Visual visual, BitmapBlendingMode value) { - element.SetValue(BitmapInterpolationModeProperty, value); + visual.RenderOptions = visual.RenderOptions with { BitmapBlendingMode = value }; + } + + /// + /// Gets the value of the EdgeMode attached property for a visual. + /// + /// The control. + /// The control's left coordinate. + public static EdgeMode GetEdgeMode(Visual visual) + { + return visual.RenderOptions.EdgeMode; + } + + /// + /// Sets the value of the EdgeMode attached property for a visual. + /// + /// The control. + /// The left value. + public static void SetEdgeMode(Visual visual, EdgeMode value) + { + visual.RenderOptions = visual.RenderOptions with { EdgeMode = value }; + } + + /// + /// Gets the value of the TextRenderingMode attached property for a visual. + /// + /// The control. + /// The control's left coordinate. + public static TextRenderingMode GetTextRenderingMode(Visual visual) + { + return visual.RenderOptions.TextRenderingMode; + } + + /// + /// Sets the value of the TextRenderingMode attached property for a visual. + /// + /// The control. + /// The left value. + public static void SetTextRenderingMode(Visual visual, TextRenderingMode value) + { + visual.RenderOptions = visual.RenderOptions with { TextRenderingMode = value }; + } + + public RenderOptions MergeWith(RenderOptions other) + { + var bitmapInterpolationMode = BitmapInterpolationMode; + + if (bitmapInterpolationMode == BitmapInterpolationMode.Unspecified) + { + bitmapInterpolationMode = other.BitmapInterpolationMode; + } + + var edgeMode = EdgeMode; + + if (edgeMode == EdgeMode.Unspecified) + { + edgeMode = other.EdgeMode; + } + + var textRenderingMode = TextRenderingMode; + + if (textRenderingMode == TextRenderingMode.Unspecified) + { + textRenderingMode = other.TextRenderingMode; + } + + var bitmapBlendingMode = BitmapBlendingMode; + + if (bitmapBlendingMode == BitmapBlendingMode.Unspecified) + { + bitmapBlendingMode = other.BitmapBlendingMode; + } + + return new RenderOptions + { + BitmapInterpolationMode = bitmapInterpolationMode, + EdgeMode = edgeMode, + TextRenderingMode = textRenderingMode, + BitmapBlendingMode = bitmapBlendingMode + }; } } } diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index e89a7d8826..8661959aa6 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Avalonia.Collections; -using Avalonia.Collections.Pooled; using Avalonia.Media.TextFormatting; -using Avalonia.Platform; -using Avalonia.Utilities; namespace Avalonia.Media { @@ -218,7 +214,7 @@ namespace Avalonia.Media { var offsetY = glyphRun.BaselineOrigin.Y - origin.Y; - var intersections = glyphRun.PlatformImpl.Item.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); + var intersections = glyphRun.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); if (intersections.Count > 0) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 253c7075fa..7d4fac337d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -185,7 +185,9 @@ namespace Avalonia.Media.TextFormatting } //Stop at the first missing glyph - if (!currentCodepoint.IsBreakChar && !glyphTypeface.TryGetGlyph(currentCodepoint, out _)) + if (!currentCodepoint.IsBreakChar && + currentCodepoint.GeneralCategory != GeneralCategory.Control && + !glyphTypeface.TryGetGlyph(currentCodepoint, out _)) { break; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index a40cbf95ad..a609800fb8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -57,21 +57,21 @@ namespace Avalonia.Media.TextFormatting switch (paragraphProperties.TextWrapping) { case TextWrapping.NoWrap: - { - var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex, - textSourceLength, - paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); + { + var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex, + textSourceLength, + paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); textLine.FinalizeLine(); - return textLine; - } + return textLine; + } case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: - { - return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth, - paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); - } + { + return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth, + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); + } default: throw new ArgumentOutOfRangeException(nameof(paragraphProperties.TextWrapping)); } @@ -568,9 +568,9 @@ namespace Avalonia.Media.TextFormatting return false; } - private static bool TryMeasureLength(IReadOnlyList textRuns, double paragraphWidth, out int measuredLength) + private static int MeasureLength(IReadOnlyList textRuns, double paragraphWidth) { - measuredLength = 0; + var measuredLength = 0; var currentWidth = 0.0; for (var i = 0; i < textRuns.Count; ++i) @@ -583,25 +583,59 @@ namespace Avalonia.Media.TextFormatting { if (shapedTextCharacters.ShapedBuffer.Length > 0) { - var firstCluster = shapedTextCharacters.ShapedBuffer[0].GlyphCluster; - var lastCluster = firstCluster; + var runLength = 0; for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++) { - var glyphInfo = shapedTextCharacters.ShapedBuffer[j]; + var currentInfo = shapedTextCharacters.ShapedBuffer[j]; + + var clusterWidth = currentInfo.GlyphAdvance; - if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) + GlyphInfo nextInfo = default; + + while (j + 1 < shapedTextCharacters.ShapedBuffer.Length) { - measuredLength += Math.Max(0, lastCluster - firstCluster); + nextInfo = shapedTextCharacters.ShapedBuffer[j + 1]; + + if (currentInfo.GlyphCluster == nextInfo.GlyphCluster) + { + clusterWidth += nextInfo.GlyphAdvance; + + j++; + + continue; + } + + break; + } - return measuredLength != 0; + var clusterLength = Math.Max(0, nextInfo.GlyphCluster - currentInfo.GlyphCluster); + + if(clusterLength == 0) + { + clusterLength = currentRun.Length - runLength; + } + + if(clusterLength == 0) + { + clusterLength = shapedTextCharacters.GlyphRun.Metrics.FirstCluster + currentRun.Length - currentInfo.GlyphCluster; + } + + if (currentWidth + clusterWidth > paragraphWidth) + { + if (runLength == 0 && measuredLength == 0) + { + runLength = clusterLength; + } + + return measuredLength + runLength; } - lastCluster = glyphInfo.GlyphCluster; - currentWidth += glyphInfo.GlyphAdvance; + currentWidth += clusterWidth; + runLength += clusterLength; } - measuredLength += currentRun.Length; + measuredLength += runLength; } break; @@ -611,7 +645,7 @@ namespace Avalonia.Media.TextFormatting { if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) { - return measuredLength != 0; + return measuredLength; } measuredLength += currentRun.Length; @@ -628,7 +662,7 @@ namespace Avalonia.Media.TextFormatting } } - return measuredLength != 0; + return measuredLength; } /// @@ -675,9 +709,11 @@ namespace Avalonia.Media.TextFormatting return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); } - if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) + var measuredLength = MeasureLength(textRuns, paragraphWidth); + + if(measuredLength == 0) { - measuredLength = 1; + } var currentLength = 0; @@ -798,6 +834,12 @@ namespace Avalonia.Media.TextFormatting continue; } + //We don't want to surpass the measuredLength with trailing whitespace when we are in a right to left setting. + if(currentPosition > measuredLength && resolvedFlowDirection == FlowDirection.RightToLeft) + { + break; + } + measuredLength = currentPosition; break; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index a382416b8a..f373e0178a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -174,7 +174,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textLine in _textLines) { - textLine.Draw(context, new Point(currentX + textLine.Start, currentY)); + textLine.Draw(context, new Point(currentX, currentY)); currentY += textLine.Height; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 3264d5e88a..1234067844 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -83,7 +83,7 @@ namespace Avalonia.Media.TextFormatting /// public override void Draw(DrawingContext drawingContext, Point lineOrigin) { - var (currentX, currentY) = lineOrigin; + var (currentX, currentY) = lineOrigin + new Point(Start, 0); foreach (var textRun in _textRuns) { @@ -698,7 +698,7 @@ namespace Avalonia.Media.TextFormatting i = lastRunIndex; //Possible overlap at runs of different direction - if (directionalWidth == 0) + if (directionalWidth == 0 && i < _textRuns.Length - 1) { //In case a run only contains a linebreak we don't want to skip it. if (currentRun is ShapedTextRun shaped) @@ -844,7 +844,7 @@ namespace Avalonia.Media.TextFormatting i = firstRunIndex; //Possible overlap at runs of different direction - if (directionalWidth == 0) + if (directionalWidth == 0 && i > 0) { //In case a run only contains a linebreak we don't want to skip it. if (currentRun is ShapedTextRun shaped) @@ -860,8 +860,8 @@ namespace Avalonia.Media.TextFormatting } } - TextBounds? textBounds = null; int coveredLength; + TextBounds? textBounds; switch (currentDirection) { @@ -942,6 +942,13 @@ namespace Avalonia.Media.TextFormatting new TextRunBounds( new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); } + else + { + //Add potential TextEndOfParagraph + textRunBounds.Add( + new TextRunBounds( + new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun)); + } currentPosition += currentRun.Length; @@ -1007,6 +1014,13 @@ namespace Avalonia.Media.TextFormatting endX += drawableTextRun.Size.Width; } + else + { + //Add potential TextEndOfParagraph + textRunBounds.Add( + new TextRunBounds( + new Rect(endX, 0, 0, Height), currentPosition, currentRun.Length, currentRun)); + } currentPosition += currentRun.Length; @@ -1409,8 +1423,6 @@ namespace Avalonia.Media.TextFormatting var fontMetrics = _paragraphProperties.DefaultTextRunProperties.CachedGlyphTypeface.Metrics; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight; - - var width = 0d; var widthIncludingWhitespace = 0d; var trailingWhitespaceLength = 0; var newLineLength = 0; @@ -1422,13 +1434,6 @@ namespace Avalonia.Media.TextFormatting var lineHeight = _paragraphProperties.LineHeight; - var lastRunIndex = _textRuns.Length - 1; - - if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) - { - lastRunIndex--; - } - for (var index = 0; index < _textRuns.Length; index++) { switch (_textRuns[index]) @@ -1486,7 +1491,7 @@ namespace Avalonia.Media.TextFormatting } } - width = widthIncludingWhitespace; + var width = widthIncludingWhitespace; for (var i = _textRuns.Length - 1; i >= 0; i--) { diff --git a/src/Avalonia.Base/Media/TextRenderingMode.cs b/src/Avalonia.Base/Media/TextRenderingMode.cs new file mode 100644 index 0000000000..927d2bce73 --- /dev/null +++ b/src/Avalonia.Base/Media/TextRenderingMode.cs @@ -0,0 +1,11 @@ +namespace Avalonia.Media +{ + public enum TextRenderingMode : byte + { + Unspecified, + + SubpixelAntialias, + Antialias, + Alias + } +} diff --git a/src/Avalonia.Base/Media/TileBrush.cs b/src/Avalonia.Base/Media/TileBrush.cs index ab1ee2d604..d7b818a174 100644 --- a/src/Avalonia.Base/Media/TileBrush.cs +++ b/src/Avalonia.Base/Media/TileBrush.cs @@ -83,7 +83,6 @@ namespace Avalonia.Media SourceRectProperty, StretchProperty, TileModeProperty); - RenderOptions.BitmapInterpolationModeProperty.OverrideDefaultValue(BitmapInterpolationMode.Default); } /// @@ -140,17 +139,5 @@ namespace Avalonia.Media get { return (TileMode)GetValue(TileModeProperty); } set { SetValue(TileModeProperty, value); } } - - /// - /// Gets or sets the bitmap interpolation mode. - /// - /// - /// The bitmap interpolation mode. - /// - public BitmapInterpolationMode BitmapInterpolationMode - { - get { return RenderOptions.GetBitmapInterpolationMode(this); } - set { RenderOptions.SetBitmapInterpolationMode(this, value); } - } } } diff --git a/src/Avalonia.Base/Media/TransformGroup.cs b/src/Avalonia.Base/Media/TransformGroup.cs index 0465efd5a5..ae5e54c414 100644 --- a/src/Avalonia.Base/Media/TransformGroup.cs +++ b/src/Avalonia.Base/Media/TransformGroup.cs @@ -11,6 +11,8 @@ namespace Avalonia.Media public static readonly StyledProperty ChildrenProperty = AvaloniaProperty.Register(nameof(Children)); + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", + Justification = "Collection properties shouldn't be set with SetCurrentValue.")] public TransformGroup() { Children = new Transforms(); diff --git a/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs b/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs new file mode 100644 index 0000000000..d523808d32 --- /dev/null +++ b/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs @@ -0,0 +1,23 @@ +using System; + +namespace Avalonia.Metadata; + +/// +/// Defines how compiler should split avalonia list string value before parsing individual items. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public sealed class AvaloniaListAttribute : Attribute +{ + /// + /// Separator used to split input string. + /// Default value is ','. + /// + public string[]? Separators { get; init; } + + /// + /// Split options used to split input string. + /// Default value is RemoveEmptyEntries with TrimEntries. + /// + // StringSplitOptions.TrimEntries = 2, but only on net6 target. + public StringSplitOptions SplitOptions { get; init; } = StringSplitOptions.RemoveEmptyEntries | (StringSplitOptions)2; +} diff --git a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs new file mode 100644 index 0000000000..3f60940c5e --- /dev/null +++ b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Metadata; + +[AttributeUsage(AttributeTargets.Interface)] +public sealed class PrivateApiAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Metadata/UnstableAttribute.cs b/src/Avalonia.Base/Metadata/UnstableAttribute.cs index 361f6d30fd..bbb298f7a6 100644 --- a/src/Avalonia.Base/Metadata/UnstableAttribute.cs +++ b/src/Avalonia.Base/Metadata/UnstableAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Avalonia.Metadata { @@ -9,5 +9,25 @@ namespace Avalonia.Metadata [AttributeUsage(AttributeTargets.All)] public sealed class UnstableAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + public UnstableAttribute() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The text string that describes alternative workarounds. + public UnstableAttribute(string? message) + { + Message = message; + } + + /// + /// Gets a value that indicates whether the compiler will treat usage of the obsolete program element as an error. + /// + public string? Message { get; } } } diff --git a/src/Avalonia.Base/Platform/AssetLoader.cs b/src/Avalonia.Base/Platform/AssetLoader.cs index 7df446e854..854610f1c9 100644 --- a/src/Avalonia.Base/Platform/AssetLoader.cs +++ b/src/Avalonia.Base/Platform/AssetLoader.cs @@ -1,281 +1,48 @@ -using System; +using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using System.Reflection; -#if !BUILDTASK -using Avalonia.Platform.Internal; -using Avalonia.Utilities; -#endif -namespace Avalonia.Platform -{ - /// - /// Loads assets compiled into the application binary. - /// - public class AssetLoader +namespace Avalonia.Platform; + #if !BUILDTASK - : IAssetLoader +/// #endif - { +public static class AssetLoader +{ #if !BUILDTASK - private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver(); - - private AssemblyDescriptor? _defaultResmAssembly; - - /// - /// Introduced for tests. - /// - internal static void SetAssemblyDescriptorResolver(IAssemblyDescriptorResolver resolver) => - s_assemblyDescriptorResolver = resolver; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The default assembly from which to load resm: assets for which no assembly is specified. - /// - public AssetLoader(Assembly? assembly = null) - { - if (assembly == null) - assembly = Assembly.GetEntryAssembly(); - if (assembly != null) - _defaultResmAssembly = new AssemblyDescriptor(assembly); - } - - /// - /// Sets the default assembly from which to load assets for which no assembly is specified. - /// - /// The default assembly. - public void SetDefaultAssembly(Assembly assembly) - { - _defaultResmAssembly = new AssemblyDescriptor(assembly); - } - - /// - /// Checks if an asset with the specified URI exists. - /// - /// The URI. - /// - /// A base URI to use if is relative. - /// - /// True if the asset could be found; otherwise false. - public bool Exists(Uri uri, Uri? baseUri = null) - { - return TryGetAsset(uri, baseUri, out _); - } - - /// - /// Opens the asset with the requested URI. - /// - /// The URI. - /// - /// A base URI to use if is relative. - /// - /// A stream containing the asset contents. - /// - /// The asset could not be found. - /// - public Stream Open(Uri uri, Uri? baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1; - - /// - /// Opens the asset with the requested URI and returns the asset stream and the - /// assembly containing the asset. - /// - /// The URI. - /// - /// A base URI to use if is relative. - /// - /// - /// The stream containing the resource contents together with the assembly. - /// - /// - /// The asset could not be found. - /// - public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) - { - if (TryGetAsset(uri, baseUri, out var assetDescriptor)) - { - return (assetDescriptor.GetStream(), assetDescriptor.Assembly); - } - - throw new FileNotFoundException($"The resource {uri} could not be found."); - } - - public Assembly? GetAssembly(Uri uri, Uri? baseUri) - { - if (!uri.IsAbsoluteUri && baseUri != null) - { - uri = new Uri(baseUri, uri); - } - - if (TryGetAssembly(uri, out var assemblyDescriptor)) - { - return assemblyDescriptor.Assembly; - } - - return null; - } - - /// - /// Gets all assets of a folder and subfolders that match specified uri. - /// - /// The URI. - /// Base URI that is used if is relative. - /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset - public IEnumerable GetAssets(Uri uri, Uri? baseUri) - { - if (uri.IsAbsoluteResm()) - { - if (!TryGetAssembly(uri, out var assembly)) - { - assembly = _defaultResmAssembly; - } - - return assembly?.Resources? - .Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath())) - .Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? - Enumerable.Empty(); - } - - uri = uri.EnsureAbsolute(baseUri); - - if (uri.IsAvares()) - { - if (!TryGetResAsmAndPath(uri, out var assembly, out var path)) - { - return Enumerable.Empty(); - } + private static IAssetLoader GetAssetLoader() => AvaloniaLocator.Current.GetRequiredService(); - if (assembly?.AvaloniaResources == null) - { - return Enumerable.Empty(); - } + /// + public static void SetDefaultAssembly(Assembly assembly) => GetAssetLoader().SetDefaultAssembly(assembly); - if (path.Length > 0 && path[path.Length - 1] != '/') - { - path += '/'; - } + /// + public static bool Exists(Uri uri, Uri? baseUri = null) => GetAssetLoader().Exists(uri, baseUri); - return assembly.AvaloniaResources - .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) - .Select(x => new Uri($"avares://{assembly.Name}{x.Key}")); - } + /// + public static Stream Open(Uri uri, Uri? baseUri = null) => GetAssetLoader().Open(uri, baseUri); - return Enumerable.Empty(); - } + /// + public static (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) + => GetAssetLoader().OpenAndGetAssembly(uri, baseUri); - private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor) - { - assetDescriptor = null; + /// + public static Assembly? GetAssembly(Uri uri, Uri? baseUri = null) + => GetAssetLoader().GetAssembly(uri, baseUri); - if (uri.IsAbsoluteResm()) - { - if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly)) - { - assembly = _defaultResmAssembly; - } - - if (assembly?.Resources != null) - { - var resourceKey = uri.AbsolutePath; - - if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor)) - { - return true; - } - } - } - - uri = uri.EnsureAbsolute(baseUri); - - if (uri.IsAvares()) - { - if (TryGetResAsmAndPath(uri, out var assembly, out var path)) - { - if (assembly.AvaloniaResources == null) - { - return false; - } - - if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor)) - { - return true; - } - } - } - - return false; - } - - private static bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path) - { - path = uri.GetUnescapeAbsolutePath(); - - if (TryLoadAssembly(uri.Authority, out assembly)) - { - return true; - } - - return false; - } - - private static bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) - { - assembly = null; - - if (uri != null) - { - if (!uri.IsAbsoluteUri) - { - return false; - } - - if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _)) - { - return true; - } - - if (uri.IsResm()) - { - var assemblyName = uri.GetAssemblyNameFromQuery(); - - if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly)) - { - return true; - } - } - } - - return false; - } - - private static bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) - { - assembly = null; - - try - { - assembly = s_assemblyDescriptorResolver.GetAssembly(assemblyName); - - return true; - } - catch (Exception) { } - - return false; - } + /// + public static IEnumerable GetAssets(Uri uri, Uri? baseUri) + => GetAssetLoader().GetAssets(uri, baseUri); #endif - public static void RegisterResUriParsers() - { - if (!UriParser.IsKnownScheme("avares")) - UriParser.Register(new GenericUriParser( - GenericUriParserOptions.GenericAuthority | - GenericUriParserOptions.NoUserInfo | - GenericUriParserOptions.NoPort | - GenericUriParserOptions.NoQuery | - GenericUriParserOptions.NoFragment), "avares", -1); - } + internal static void RegisterResUriParsers() + { + if (!UriParser.IsKnownScheme("avares")) + UriParser.Register(new GenericUriParser( + GenericUriParserOptions.GenericAuthority | + GenericUriParserOptions.NoUserInfo | + GenericUriParserOptions.NoPort | + GenericUriParserOptions.NoQuery | + GenericUriParserOptions.NoFragment), "avares", -1); } } diff --git a/src/Avalonia.Base/Platform/IAssetLoader.cs b/src/Avalonia.Base/Platform/IAssetLoader.cs index b65d61803f..f1ce624c70 100644 --- a/src/Avalonia.Base/Platform/IAssetLoader.cs +++ b/src/Avalonia.Base/Platform/IAssetLoader.cs @@ -9,7 +9,7 @@ namespace Avalonia.Platform /// /// Loads assets compiled into the application binary. /// - [Unstable] + [Unstable("IAssetLoader interface and AvaloniaLocator usage is considered unstable. Please use AssetLoader static class instead.")] public interface IAssetLoader { /// diff --git a/src/Avalonia.Base/Platform/ICursorFactory.cs b/src/Avalonia.Base/Platform/ICursorFactory.cs index fff1f92d53..99a9a9d7fa 100644 --- a/src/Avalonia.Base/Platform/ICursorFactory.cs +++ b/src/Avalonia.Base/Platform/ICursorFactory.cs @@ -1,9 +1,11 @@ using Avalonia.Input; +using Avalonia.Metadata; #nullable enable namespace Avalonia.Platform { + [PrivateApi] public interface ICursorFactory { ICursorImpl GetCursor(StandardCursorType cursorType); diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index ffdfa9aac1..d86519656c 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -1,9 +1,8 @@ using System; using Avalonia.Media; -using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; -using Avalonia.Media.Imaging; using Avalonia.Metadata; +using Avalonia.Media.Imaging; namespace Avalonia.Platform { @@ -13,6 +12,11 @@ namespace Avalonia.Platform [Unstable] public interface IDrawingContextImpl : IDisposable { + /// + /// Gets or sets the current render options used to control the rendering behavior of drawing operations. + /// + RenderOptions RenderOptions { get; set; } + /// /// Gets or sets the current transform of the drawing context. /// @@ -31,8 +35,7 @@ namespace Avalonia.Platform /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - /// The bitmap interpolation mode. - void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); + void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect); /// /// Draws a bitmap image. @@ -158,21 +161,6 @@ namespace Avalonia.Platform void PopGeometryClip(); /// - /// Pushes a bitmap blending value. - /// - /// The bitmap blending mode. - void PushBitmapBlendMode(BitmapBlendingMode blendingMode); - - /// - /// Pops the latest pushed bitmap blending value. - /// - void PopBitmapBlendMode(); - - /// - /// Adds a custom draw operation - /// - /// Custom draw operation - void Custom(ICustomDrawOperation custom); /// /// Attempts to get an optional feature from the drawing context implementation @@ -180,6 +168,12 @@ namespace Avalonia.Platform object? GetFeature(Type t); } + public interface IDrawingContextImplWithEffects + { + void PushEffect(IEffect effect); + void PopEffect(); + } + public static class DrawingContextImplExtensions { /// diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs index 116f7cd6e2..222e7196bb 100644 --- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs @@ -27,15 +27,13 @@ namespace Avalonia.Platform /// The font style. /// The font weight. /// The font stretch. - /// The font family. This is optional and used for fallback lookup. /// The culture. /// The matching typeface. /// /// True, if the could match the character to specified parameters, False otherwise. /// bool TryMatchCharacter(int codepoint, FontStyle fontStyle, - FontWeight fontWeight, FontStretch fontStretch, - FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface); + FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface); /// /// Tries to get a glyph typeface for specified parameters. diff --git a/src/Avalonia.Base/Platform/IGlyphRunBuffer.cs b/src/Avalonia.Base/Platform/IGlyphRunBuffer.cs deleted file mode 100644 index c1fc7a5967..0000000000 --- a/src/Avalonia.Base/Platform/IGlyphRunBuffer.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Drawing; - -namespace Avalonia.Platform -{ - public interface IGlyphRunBuffer - { - Span GlyphIndices { get; } - - IGlyphRunImpl Build(); - } - - public interface IHorizontalGlyphRunBuffer : IGlyphRunBuffer - { - Span GlyphPositions { get; } - } - - public interface IPositionedGlyphRunBuffer : IGlyphRunBuffer - { - Span GlyphPositions { get; } - } -} diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index fccea27c43..2342f32307 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -1,25 +1,36 @@ using System; using System.Collections.Generic; +using Avalonia.Media; using Avalonia.Metadata; namespace Avalonia.Platform { /// - /// Actual implementation of a glyph run that stores platform dependent resources. + /// An immutable platform representation of a . /// [Unstable] - public interface IGlyphRunImpl : IDisposable + public interface IGlyphRunImpl : IDisposable { /// - /// Gets the conservative bounding box of the glyph run./>. + /// Gets the for the . /// - Rect Bounds { get; } + IGlyphTypeface GlyphTypeface { get; } + + /// + /// Gets the em size used for rendering the . + /// + double FontRenderingEmSize { get; } /// /// Gets the baseline origin of the glyph run./>. /// Point BaselineOrigin { get; } + /// + /// Gets the conservative bounding box of the glyph run./>. + /// + Rect Bounds { get; } + /// /// Gets the intersections of specified upper and lower limit. /// diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 81fe2c046f..b0d17f9c85 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -11,7 +11,7 @@ namespace Avalonia.Platform /// /// Defines the main platform-specific interface for the rendering subsystem. /// - [Unstable] + [Unstable, PrivateApi] public interface IPlatformRenderInterface { /// @@ -169,8 +169,9 @@ namespace Avalonia.Platform /// The font rendering em size. /// The list of glyphs. /// The baseline origin of the run. Can be null. + /// the conservative bounding box of the run /// An . - IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin); + IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin, Rect bounds); /// /// Creates a backend-specific object using a low-level API graphics context @@ -201,7 +202,7 @@ namespace Avalonia.Platform bool IsSupportedBitmapPixelFormat(PixelFormat format); } - [Unstable] + [Unstable, PrivateApi] public interface IPlatformRenderInterfaceContext : IOptionalFeatureProvider, IDisposable { /// diff --git a/src/Avalonia.Base/Platform/StandardAssetLoader.cs b/src/Avalonia.Base/Platform/StandardAssetLoader.cs new file mode 100644 index 0000000000..1d9363c70d --- /dev/null +++ b/src/Avalonia.Base/Platform/StandardAssetLoader.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using Avalonia.Metadata; +using Avalonia.Platform.Internal; +using Avalonia.Utilities; + +namespace Avalonia.Platform; + +/// +/// Loads assets compiled into the application binary. +/// +[Unstable("StandardAssetLoader is considered unstable. Please use AssetLoader static class instead.")] +public class StandardAssetLoader : IAssetLoader +{ + private readonly IAssemblyDescriptorResolver _assemblyDescriptorResolver; + private AssemblyDescriptor? _defaultResmAssembly; + + internal StandardAssetLoader(IAssemblyDescriptorResolver resolver, Assembly? assembly = null) + { + if (assembly == null) + assembly = Assembly.GetEntryAssembly(); + if (assembly != null) + _defaultResmAssembly = new AssemblyDescriptor(assembly); + _assemblyDescriptorResolver = resolver; + } + + public StandardAssetLoader(Assembly? assembly = null) : this(new AssemblyDescriptorResolver(), assembly) + { + + } + + /// + /// Sets the default assembly from which to load assets for which no assembly is specified. + /// + /// The default assembly. + public void SetDefaultAssembly(Assembly assembly) + { + _defaultResmAssembly = new AssemblyDescriptor(assembly); + } + + /// + /// Checks if an asset with the specified URI exists. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// True if the asset could be found; otherwise false. + public bool Exists(Uri uri, Uri? baseUri = null) + { + return TryGetAsset(uri, baseUri, out _); + } + + /// + /// Opens the asset with the requested URI. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// A stream containing the asset contents. + /// + /// The asset could not be found. + /// + public Stream Open(Uri uri, Uri? baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1; + + /// + /// Opens the asset with the requested URI and returns the asset stream and the + /// assembly containing the asset. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// + /// The stream containing the resource contents together with the assembly. + /// + /// + /// The asset could not be found. + /// + public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) + { + if (TryGetAsset(uri, baseUri, out var assetDescriptor)) + { + return (assetDescriptor.GetStream(), assetDescriptor.Assembly); + } + + throw new FileNotFoundException($"The resource {uri} could not be found."); + } + + public Assembly? GetAssembly(Uri uri, Uri? baseUri) + { + if (!uri.IsAbsoluteUri && baseUri != null) + { + uri = new Uri(baseUri, uri); + } + + if (TryGetAssembly(uri, out var assemblyDescriptor)) + { + return assemblyDescriptor.Assembly; + } + + return null; + } + + /// + /// Gets all assets of a folder and subfolders that match specified uri. + /// + /// The URI. + /// Base URI that is used if is relative. + /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset + public IEnumerable GetAssets(Uri uri, Uri? baseUri) + { + if (uri.IsAbsoluteResm()) + { + if (!TryGetAssembly(uri, out var assembly)) + { + assembly = _defaultResmAssembly; + } + + return assembly?.Resources? + .Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath())) + .Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? + Enumerable.Empty(); + } + + uri = uri.EnsureAbsolute(baseUri); + + if (uri.IsAvares()) + { + if (!TryGetResAsmAndPath(uri, out var assembly, out var path)) + { + return Enumerable.Empty(); + } + + if (assembly?.AvaloniaResources == null) + { + return Enumerable.Empty(); + } + + if (path.Length > 0 && path[path.Length - 1] != '/') + { + path += '/'; + } + + return assembly.AvaloniaResources + .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) + .Select(x => new Uri($"avares://{assembly.Name}{x.Key}")); + } + + return Enumerable.Empty(); + } + + public static void RegisterResUriParsers() => AssetLoader.RegisterResUriParsers(); + + private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor) + { + assetDescriptor = null; + + if (uri.IsAbsoluteResm()) + { + if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly)) + { + assembly = _defaultResmAssembly; + } + + if (assembly?.Resources != null) + { + var resourceKey = uri.AbsolutePath; + + if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor)) + { + return true; + } + } + } + + uri = uri.EnsureAbsolute(baseUri); + + if (uri.IsAvares()) + { + if (TryGetResAsmAndPath(uri, out var assembly, out var path)) + { + if (assembly.AvaloniaResources == null) + { + return false; + } + + if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor)) + { + return true; + } + } + } + + return false; + } + + private bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path) + { + path = uri.GetUnescapeAbsolutePath(); + + if (TryLoadAssembly(uri.Authority, out assembly)) + { + return true; + } + + return false; + } + + private bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) + { + assembly = null; + + if (uri != null) + { + if (!uri.IsAbsoluteUri) + { + return false; + } + + if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _)) + { + return true; + } + + if (uri.IsResm()) + { + var assemblyName = uri.GetAssemblyNameFromQuery(); + + if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly)) + { + return true; + } + } + } + + return false; + } + + private bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) + { + assembly = null; + + try + { + assembly = _assemblyDescriptorResolver.GetAssembly(assemblyName); + + return true; + } + catch (Exception) { } + + return false; + } +} diff --git a/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs b/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs index 0a36b4c9dd..800d9b390f 100644 --- a/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs +++ b/src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs @@ -14,7 +14,7 @@ namespace Avalonia.Platform AssetLoader.RegisterResUriParsers(); AvaloniaLocator.CurrentMutable .Bind().ToConstant(standardPlatform) - .Bind().ToConstant(new AssetLoader(assembly)) + .Bind().ToConstant(new StandardAssetLoader(assembly)) .Bind().ToConstant( #if NET6_0_OR_GREATER new Net6Loader() diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs index 608f924808..55aac6f3fa 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs @@ -50,7 +50,8 @@ internal static class StorageProviderHelpers } } - public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter) + [return: NotNullIfNotNull(nameof(path))] + public static string? NameWithExtension(string? path, string? defaultExtension, FilePickerFileType? filter) { var name = Path.GetFileName(path); if (name != null && !Path.HasExtension(name)) diff --git a/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs new file mode 100644 index 0000000000..153634027c --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform.Storage.FileIO; + +namespace Avalonia.Platform.Storage; + +internal class NoopStorageProvider : BclStorageProvider +{ + public override bool CanOpen => false; + public override Task> OpenFilePickerAsync(FilePickerOpenOptions options) + { + return Task.FromResult>(Array.Empty()); + } + + public override bool CanSave => false; + public override Task SaveFilePickerAsync(FilePickerSaveOptions options) + { + return Task.FromResult(null); + } + + public override bool CanPickFolder => false; + public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) + { + return Task.FromResult>(Array.Empty()); + } +} diff --git a/src/Avalonia.Base/Points.cs b/src/Avalonia.Base/Points.cs index b655dbcb38..2f88ecd80f 100644 --- a/src/Avalonia.Base/Points.cs +++ b/src/Avalonia.Base/Points.cs @@ -1,6 +1,18 @@ +using System.Collections.Generic; using Avalonia.Collections; namespace Avalonia { - public sealed class Points : AvaloniaList { } + public sealed class Points : AvaloniaList + { + public Points() + { + + } + + public Points(IEnumerable points) : base(points) + { + + } + } } diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index cc030eea04..fc5d0fc043 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/src/Avalonia.Base/Rect.cs @@ -526,6 +526,15 @@ namespace Avalonia } } + internal static Rect? Union(Rect? left, Rect? right) + { + if (left == null) + return right; + if (right == null) + return left; + return left.Value.Union(right.Value); + } + /// /// Returns a new with the specified X position. /// diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index df3a70b3e6..1bf52729a0 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -252,8 +252,16 @@ public class CompositingRenderer : IRendererWithCompositor comp.Opacity = (float)visual.Opacity; comp.ClipToBounds = visual.ClipToBounds; comp.Clip = visual.Clip?.PlatformImpl; - comp.OpacityMask = visual.OpacityMask; - + + + if (!Equals(comp.OpacityMask, visual.OpacityMask)) + comp.OpacityMask = visual.OpacityMask?.ToImmutable(); + + if (!comp.Effect.EffectEquals(visual.Effect)) + comp.Effect = visual.Effect?.ToImmutable(); + + comp.RenderOptions = visual.RenderOptions; + var renderTransform = Matrix.Identity; if (visual.HasMirrorTransform) @@ -266,8 +274,6 @@ public class CompositingRenderer : IRendererWithCompositor renderTransform *= (-offset) * visual.RenderTransform.Value * (offset); } - - comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); _recorder.BeginUpdate(comp.DrawList); diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index f81cc5a1a0..ec419e6313 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -78,15 +78,14 @@ internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContex } } - internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) { var next = NextDrawAs(); if (next == null || - !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) + !next.Item.Equals(Transform, source, opacity, sourceRect, destRect)) { - Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); + Add(new ImageNode(Transform, source, opacity, sourceRect, destRect)); } else { @@ -227,20 +226,6 @@ internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContex } } - protected override void PopBitmapBlendModeCore() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new BitmapBlendModeNode()); - } - else - { - ++_drawOperationIndex; - } - } - protected override void PopOpacityCore() { var next = NextDrawAs(); @@ -354,21 +339,6 @@ internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContex _needsToPopOpacityMask.Push(needsToPop); } - /// - protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(blendingMode)) - { - Add(new BitmapBlendModeNode(blendingMode)); - } - else - { - ++_drawOperationIndex; - } - } - private void Add(T node) where T : class, IDrawOperation { if (_drawOperationIndex < _builder.Count) diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs index bb7372c375..3991300fb5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs @@ -29,6 +29,8 @@ namespace Avalonia.Rendering.Composition.Expressions } } + public bool NextIsWhitespace() => _s.Length > 0 && char.IsWhiteSpace(_s[0]); + static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); @@ -238,6 +240,12 @@ namespace Avalonia.Rendering.Composition.Expressions len = c + 1; dotCount++; } + else if (ch == '-') + { + if (len != 0) + break; + len = c + 1; + } else break; } @@ -254,7 +262,55 @@ namespace Avalonia.Rendering.Composition.Expressions Advance(len); return true; } + + public bool TryParseDouble(out double res) + { + res = 0; + SkipWhitespace(); + if (_s.Length == 0) + return false; + + var len = 0; + var dotCount = 0; + for (var c = 0; c < _s.Length; c++) + { + var ch = _s[c]; + if (ch >= '0' && ch <= '9') + len = c + 1; + else if (ch == '.' && dotCount == 0) + { + len = c + 1; + dotCount++; + } + else if (ch == '-') + { + if (len != 0) + return false; + len = c + 1; + } + else + break; + } + + var span = _s.Slice(0, len); + +#if NETSTANDARD2_0 + if (!double.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#else + if (!double.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res)) + return false; +#endif + Advance(len); + return true; + } + public bool IsEofWithWhitespace() + { + SkipWhitespace(); + return Length == 0; + } + public override string ToString() => _s.ToString(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index eaa9a70ca0..5a4890e568 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -18,7 +18,8 @@ namespace Avalonia.Rendering.Composition.Server; /// they have information about the full render transform (they are not) /// 2) Keeps the draw list for the VisualBrush contents of the current drawing operation. /// -internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport +internal class CompositorDrawingContextProxy : IDrawingContextImpl, + IDrawingContextWithAcrylicLikeSupport, IDrawingContextImplWithEffects { private IDrawingContextImpl _impl; @@ -41,15 +42,20 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont set => _impl.Transform = (_transform = value) * PostTransform; } + public RenderOptions RenderOptions + { + get => _impl.RenderOptions; + set => _impl.RenderOptions = value; + } + public void Clear(Color color) { _impl.Clear(color); } - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) { - _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + _impl.DrawBitmap(source, opacity, sourceRect, destRect); } public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) @@ -132,27 +138,24 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.PopGeometryClip(); } - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) - { - _impl.PushBitmapBlendMode(blendingMode); - } + public object? GetFeature(Type t) => _impl.GetFeature(t); + - public void PopBitmapBlendMode() + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) { - _impl.PopBitmapBlendMode(); + if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic) + acrylic.DrawRectangle(material, rect); } - public void Custom(ICustomDrawOperation custom) + public void PushEffect(IEffect effect) { - _impl.Custom(custom); + if (_impl is IDrawingContextImplWithEffects effects) + effects.PushEffect(effect); } - public object? GetFeature(Type t) => _impl.GetFeature(t); - - - public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + public void PopEffect() { - if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic) - acrylic.DrawRectangle(material, rect); + if (_impl is IDrawingContextImplWithEffects effects) + effects.PopEffect(); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs index 19349a5196..b9e6833d21 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs @@ -1,4 +1,6 @@ +using System; using System.Numerics; +using Avalonia.Media; using Avalonia.Platform; // Special license applies License.md @@ -13,6 +15,8 @@ namespace Avalonia.Rendering.Composition.Server internal partial class ServerCompositionContainerVisual : ServerCompositionVisual { public ServerCompositionVisualCollection Children { get; private set; } = null!; + private Rect? _transformedContentBounds; + private IImmutableEffect? _oldEffect; protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) { @@ -24,18 +28,76 @@ namespace Avalonia.Rendering.Composition.Server } } - public override void Update(ServerCompositionTarget root) + public override UpdateResult Update(ServerCompositionTarget root) { - base.Update(root); + var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root); foreach (var child in Children) { if (child.AdornedVisual != null) root.EnqueueAdornerUpdate(child); else - child.Update(root); + { + var res = child.Update(root); + oldInvalidated |= res.InvalidatedOld; + newInvalidated |= res.InvalidatedNew; + combinedBounds = Rect.Union(combinedBounds, res.Bounds); + } } + + // If effect is changed, we need to clean both old and new bounds + var effectChanged = !Effect.EffectEquals(_oldEffect); + if (effectChanged) + oldInvalidated = newInvalidated = true; + + // Expand invalidated bounds to the whole content area since we don't actually know what is being sampled + // We also ignore clip for now since we don't have means to reset it? + if (_oldEffect != null && oldInvalidated && _transformedContentBounds.HasValue) + AddEffectPaddedDirtyRect(_oldEffect, _transformedContentBounds.Value); + + if (Effect != null && newInvalidated && combinedBounds.HasValue) + AddEffectPaddedDirtyRect(Effect, combinedBounds.Value); + + _oldEffect = Effect; + _transformedContentBounds = combinedBounds; IsDirtyComposition = false; + return new(_transformedContentBounds, oldInvalidated, newInvalidated); + } + + void AddEffectPaddedDirtyRect(IImmutableEffect effect, Rect transformedBounds) + { + var padding = effect.GetEffectOutputPadding(); + if (padding == default) + { + AddDirtyRect(transformedBounds); + return; + } + + // We are in a weird position here: bounds are in global coordinates while padding gets applied in local ones + // Since we have optimizations to AVOID recomputing transformed bounds and since visuals with effects are relatively rare + // we instead apply the transformation matrix to rescale the bounds + + + // If we only have translation and scale, just scale the padding + if (CombinedTransformMatrix is + { + M12: 0, M13: 0, M14: 0, + M21: 0, M23: 0, M24: 0, + M31: 0, M32: 0, M34: 0, + M43: 0, M44: 1 + }) + padding = new Thickness(padding.Left * CombinedTransformMatrix.M11, + padding.Top * CombinedTransformMatrix.M22, + padding.Right * CombinedTransformMatrix.M11, + padding.Bottom * CombinedTransformMatrix.M22); + else + { + // Conservatively use the transformed rect size + var transformedPaddingRect = new Rect().Inflate(padding).TransformToAABB(CombinedTransformMatrix); + padding = new(Math.Max(transformedPaddingRect.Width, transformedPaddingRect.Height)); + } + + AddDirtyRect(transformedBounds.Inflate(padding)); } partial void Initialize() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 3e88b9e77b..45275bdfe1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -181,7 +181,7 @@ namespace Avalonia.Rendering.Composition.Server else targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, new Rect(_layerSize), - new Rect(Size), BitmapInterpolationMode.LowQuality); + new Rect(Size)); if (DebugOverlays != RendererDebugOverlays.None) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index 6fb5ad3741..853b90be5e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -54,6 +54,9 @@ namespace Avalonia.Rendering.Composition.Server canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; + if (Effect != null) + canvas.PushEffect(Effect); + if (Opacity != 1) canvas.PushOpacity(Opacity, boundsRect); if (ClipToBounds && !HandlesClipToBounds) @@ -63,6 +66,8 @@ namespace Avalonia.Rendering.Composition.Server if (OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); + canvas.RenderOptions = RenderOptions; + RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix @@ -79,6 +84,9 @@ namespace Avalonia.Rendering.Composition.Server canvas.PopClip(); if (Opacity != 1) canvas.PopOpacity(); + + if (Effect != null) + canvas.PopEffect(); } protected virtual bool HandlesClipToBounds => false; @@ -101,13 +109,26 @@ namespace Avalonia.Rendering.Composition.Server public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity; public Matrix4x4 GlobalTransformMatrix { get; private set; } - public virtual void Update(ServerCompositionTarget root) + public record struct UpdateResult(Rect? Bounds, bool InvalidatedOld, bool InvalidatedNew) + { + public UpdateResult() : this(null, false, false) + { + + } + } + + public virtual UpdateResult Update(ServerCompositionTarget root) { if (Parent == null && Root == null) - return; + return default; var wasVisible = IsVisibleInFrame; + if(Parent != null) + { + RenderOptions = RenderOptions.MergeWith(Parent.RenderOptions); + } + // Calculate new parent-relative transform if (_combinedTransformDirty) { @@ -146,6 +167,11 @@ namespace Avalonia.Rendering.Composition.Server GlobalTransformMatrix = newTransform; var ownBounds = OwnContentBounds; + + // Since padding is applied in the current visual's coordinate space we expand bounds before transforming them + if (Effect != null) + ownBounds = ownBounds.Inflate(Effect.GetEffectOutputPadding()); + if (ownBounds != _oldOwnContentBounds || positionChanged) { _oldOwnContentBounds = ownBounds; @@ -168,7 +194,7 @@ namespace Avalonia.Rendering.Composition.Server _combinedTransformedClipBounds = AdornedVisual?._combinedTransformedClipBounds - ?? Parent?._combinedTransformedClipBounds + ?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null) ?? new Rect(Root!.Size); if (_transformedClipBounds != null) @@ -208,9 +234,10 @@ namespace Avalonia.Rendering.Composition.Server readback.Matrix = GlobalTransformMatrix; readback.TargetId = Root.Id; readback.Visible = IsHitTestVisibleInFrame; + return new(TransformedOwnContentBounds, invalidateNewBounds, invalidateOldBounds); } - void AddDirtyRect(Rect rc) + protected void AddDirtyRect(Rect rc) { if (rc == default) return; diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 9b7d358b1d..3462b1008a 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -44,79 +44,99 @@ namespace Avalonia.Rendering public static void Render(DrawingContext context, Visual visual, Rect clipRect) { - var opacity = visual.Opacity; - var clipToBounds = visual.ClipToBounds; - var bounds = new Rect(visual.Bounds.Size); + var currentRenderOptions = default(RenderOptions); + var platformContext = context as PlatformDrawingContext; - if (visual.IsVisible && opacity > 0) + try { - var m = Matrix.CreateTranslation(visual.Bounds.Position); - - var renderTransform = Matrix.Identity; - - // this should be calculated BEFORE renderTransform - if (visual.HasMirrorTransform) + if (platformContext != null) { - var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); - renderTransform *= mirrorMatrix; - } + currentRenderOptions = platformContext.RenderOptions; - if (visual.RenderTransform != null) - { - var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); - var offset = Matrix.CreateTranslation(origin); - var finalTransform = (-offset) * visual.RenderTransform.Value * (offset); - renderTransform *= finalTransform; + platformContext.RenderOptions = visual.RenderOptions.MergeWith(platformContext.RenderOptions); } - m = renderTransform * m; + var opacity = visual.Opacity; + var clipToBounds = visual.ClipToBounds; + var bounds = new Rect(visual.Bounds.Size); - if (clipToBounds) + if (visual.IsVisible && opacity > 0) { + var m = Matrix.CreateTranslation(visual.Bounds.Position); + + var renderTransform = Matrix.Identity; + + // this should be calculated BEFORE renderTransform + if (visual.HasMirrorTransform) + { + var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); + renderTransform *= mirrorMatrix; + } + if (visual.RenderTransform != null) { - clipRect = new Rect(visual.Bounds.Size); + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var offset = Matrix.CreateTranslation(origin); + var finalTransform = (-offset) * visual.RenderTransform.Value * (offset); + renderTransform *= finalTransform; } - else + + m = renderTransform * m; + + if (clipToBounds) { - clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size)); + if (visual.RenderTransform != null) + { + clipRect = new Rect(visual.Bounds.Size); + } + else + { + clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size)); + } } - } - using (context.PushTransform(m)) - using (context.PushOpacity(opacity, bounds)) - using (clipToBounds + using (context.PushTransform(m)) + using (context.PushOpacity(opacity, bounds)) + using (clipToBounds #pragma warning disable CS0618 // Type or member is obsolete - ? visual is IVisualWithRoundRectClip roundClipVisual - ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) - : context.PushClip(bounds) - : default) + ? visual is IVisualWithRoundRectClip roundClipVisual + ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) + : context.PushClip(bounds) + : default) #pragma warning restore CS0618 // Type or member is obsolete - using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default) - using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default) - using (context.PushTransform(Matrix.Identity)) - { - visual.Render(context); - - var childrenEnumerable = visual.HasNonUniformZIndexChildren - ? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance) - : (IEnumerable)visual.VisualChildren; - - foreach (var child in childrenEnumerable) + using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default) + using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default) + using (context.PushTransform(Matrix.Identity)) { - var childBounds = GetTransformedBounds(child); + visual.Render(context); + + var childrenEnumerable = visual.HasNonUniformZIndexChildren + ? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance) + : (IEnumerable)visual.VisualChildren; - if (!child.ClipToBounds || clipRect.Intersects(childBounds)) + foreach (var child in childrenEnumerable) { - var childClipRect = child.RenderTransform == null - ? clipRect.Translate(-childBounds.Position) - : clipRect; - Render(context, child, childClipRect); - } + var childBounds = GetTransformedBounds(child); + + if (!child.ClipToBounds || clipRect.Intersects(childBounds)) + { + var childClipRect = child.RenderTransform == null + ? clipRect.Translate(-childBounds.Position) + : clipRect; + Render(context, child, childClipRect); + } + } } } } + finally + { + if (platformContext != null) + { + platformContext.RenderOptions = currentRenderOptions; + } + } } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs deleted file mode 100644 index b1190a159b..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Avalonia.Platform; -using Avalonia.Media.Imaging; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents an bitmap blending mode push or pop. - /// - internal class BitmapBlendModeNode : IDrawOperation - { - /// - /// Initializes a new instance of the class that represents an - /// push. - /// - /// The to push. - public BitmapBlendModeNode(BitmapBlendingMode bitmapBlend) - { - BlendingMode = bitmapBlend; - } - - /// - /// Initializes a new instance of the class that represents an - /// pop. - /// - public BitmapBlendModeNode() - { - } - - /// - public Rect Bounds => default; - - /// - /// Gets the BitmapBlend to be pushed or null if the operation represents a pop. - /// - public BitmapBlendingMode? BlendingMode { get; } - - /// - public bool HitTest(Point p) => false; - - /// - /// Determines if this draw operation equals another. - /// - /// the how to compare - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(BitmapBlendingMode? blendingMode) => BlendingMode == blendingMode; - - /// - public void Render(IDrawingContextImpl context) - { - if (BlendingMode.HasValue) - { - context.PushBitmapBlendMode(BlendingMode.Value); - } - else - { - context.PopBitmapBlendMode(); - } - } - - public void Dispose() - { - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs index ff2616bfe4..7ce9e6a8af 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Logging; using Avalonia.Media; using Avalonia.Platform; @@ -13,11 +14,20 @@ namespace Avalonia.Rendering.SceneGraph Custom = custom; } - public override bool HitTest(Point p) => Custom.HitTest(p); + public override bool HitTestTransformed(Point p) => Custom.HitTest(p); public override void Render(IDrawingContextImpl context) { - Custom.Render(context); + using var immediateDrawingContext = new ImmediateDrawingContext(context, false); + try + { + Custom.Render(immediateDrawingContext); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual) + ?.Log(Custom, $"Exception in {Custom.GetType().Name}.{nameof(ICustomDrawOperation.Render)} {{0}}", e); + } } public override void Dispose() => Custom.Dispose(); @@ -48,6 +58,6 @@ namespace Avalonia.Rendering.SceneGraph /// Renders the node to a drawing context. /// /// The drawing context. - void Render(IDrawingContextImpl context); + void Render(ImmediateDrawingContext context); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs index 5b93cd8cfc..786ce28d06 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs @@ -37,5 +37,20 @@ namespace Avalonia.Rendering.SceneGraph } public Matrix Transform { get; } + + public sealed override bool HitTest(Point p) + { + if (Transform.IsIdentity) + return HitTestTransformed(p); + + if (!Transform.HasInverse) + return false; + + var transformedPoint = Transform.Invert().Transform(p); + + return HitTestTransformed(transformedPoint); + } + + public abstract bool HitTestTransformed(Point p); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index d5f0270cb2..0a2b74e46a 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -43,7 +43,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect); - public override bool HitTest(Point p) + public override bool HitTestTransformed(Point p) { var center = Rect.Center; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index e1f79e0e10..22fc49d30e 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -65,6 +65,6 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p); + public override bool HitTestTransformed(Point p) => Rect.Rect.ContainsExclusive(p); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index f64a3e845d..48af3b0e6b 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -64,7 +64,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) + public override bool HitTestTransformed(Point p) { return (Brush != null && Geometry.FillContains(p)) || (Pen != null && Geometry.StrokeContains(Pen, p)); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 5b975e29e1..764c5c65f9 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Media; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; @@ -18,7 +17,7 @@ namespace Avalonia.Rendering.SceneGraph /// The glyph run to draw. public GlyphRunNode( Matrix transform, - IImmutableBrush foreground, + IImmutableBrush? foreground, IRef glyphRun) : base(glyphRun.Item.Bounds, transform, foreground) { @@ -53,7 +52,10 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + public override bool HitTestTransformed(Point p) + { + return GlyphRun.Item.Bounds.ContainsExclusive(p); + } public override void Dispose() { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs index dd9787e8d1..caf0eee175 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs @@ -1,6 +1,5 @@ using Avalonia.Platform; using Avalonia.Utilities; -using Avalonia.Media.Imaging; namespace Avalonia.Rendering.SceneGraph { @@ -17,15 +16,13 @@ namespace Avalonia.Rendering.SceneGraph /// The draw opacity. /// The source rect. /// The destination rect. - /// The bitmap interpolation mode. - public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect) : base(destRect, transform) { Source = source.Clone(); Opacity = opacity; SourceRect = sourceRect; DestRect = destRect; - BitmapInterpolationMode = bitmapInterpolationMode; SourceVersion = Source.Item.Version; } @@ -53,14 +50,6 @@ namespace Avalonia.Rendering.SceneGraph /// Gets the destination rect. /// public Rect DestRect { get; } - - /// - /// Gets the bitmap interpolation mode. - /// - /// - /// The scaling mode. - /// - public BitmapInterpolationMode BitmapInterpolationMode { get; } /// /// Determines if this draw operation equals another. @@ -70,31 +59,29 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity of the other draw operation. /// The source rect of the other draw operation. /// The dest rect of the other draw operation. - /// The bitmap interpolation mode. /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + public bool Equals(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect) { return transform == Transform && Equals(source.Item, Source.Item) && source.Item.Version == SourceVersion && opacity == Opacity && sourceRect == SourceRect && - destRect == DestRect && - bitmapInterpolationMode == BitmapInterpolationMode; + destRect == DestRect; } /// public override void Render(IDrawingContextImpl context) { - context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); + context.DrawBitmap(Source, Opacity, SourceRect, DestRect); } /// - public override bool HitTest(Point p) => DestRect.ContainsExclusive(p); + public override bool HitTestTransformed(Point p) => DestRect.ContainsExclusive(p); public override void Dispose() { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index 61bffc3260..1ac6cffe0a 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -66,7 +66,7 @@ namespace Avalonia.Rendering.SceneGraph context.DrawLine(Pen, P1, P2); } - public override bool HitTest(Point p) + public override bool HitTestTransformed(Point p) { var halfThickness = Pen.Thickness / 2; var minX = Math.Min(P1.X, P2.X) - halfThickness; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index b0584038a8..1c79a67944 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// - public override bool HitTest(Point p) => false; + public override bool HitTestTransformed(Point p) => false; /// /// Determines if this draw operation equals another. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index 94f61df47d..e85992be34 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -74,7 +74,7 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows); /// - public override bool HitTest(Point p) + public override bool HitTestTransformed(Point p) { if (Brush != null) { diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 731cb97161..196fa850d6 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -30,10 +30,12 @@ namespace Avalonia ILogical, IThemeVariantHost, IStyleHost, - IStyleable, ISetLogicalParent, ISetInheritanceParent, - ISupportInitialize + ISupportInitialize, +#pragma warning disable CS0618 // Type or member is obsolete + IStyleable +#pragma warning restore CS0618 // Type or member is obsolete { /// /// Defines the property. @@ -78,7 +80,7 @@ namespace Avalonia private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; - private readonly Classes _classes = new Classes(); + private Classes? _classes; private ILogicalRoot? _logicalRoot; private IAvaloniaList? _logicalChildren; private IResourceDictionary? _resources; @@ -183,21 +185,7 @@ namespace Avalonia /// collection. /// /// - public Classes Classes - { - get - { - return _classes; - } - - set - { - if (_classes != value) - { - _classes.Replace(value); - } - } - } + public Classes Classes => _classes ??= new(); /// /// Gets or sets the control's data context. @@ -231,6 +219,18 @@ namespace Avalonia /// public Styles Styles => _styles ??= new Styles(this); + /// + /// Gets the type by which the element is styled. + /// + /// + /// Usually controls are styled by their own type, but there are instances where you want + /// an element to be styled by its base type, e.g. creating SpecialButton that + /// derives from Button and adds extra functionality but is still styled as a regular + /// Button. To change the style for a control class, override the + /// property + /// + public Type StyleKey => StyleKeyOverride; + /// /// Gets or sets the styled element's resource dictionary. /// @@ -292,6 +292,18 @@ namespace Avalonia /// protected IPseudoClasses PseudoClasses => Classes; + /// + /// Gets the type by which the element is styled. + /// + /// + /// Usually controls are styled by their own type, but there are instances where you want + /// an element to be styled by its base type, e.g. creating SpecialButton that + /// derives from Button and adds extra functionality but is still styled as a regular + /// Button. Override this property to change the style for a control class, returning the + /// type that you wish the elements to be styled as. + /// + protected virtual Type StyleKeyOverride => GetType(); + /// /// Gets a value indicating whether the element is attached to a rooted logical tree. /// @@ -303,6 +315,7 @@ namespace Avalonia public StyledElement? Parent { get; private set; } /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1030:StyledProperty accessors should not have side effects", Justification = "False positive?")] public ThemeVariant ActualThemeVariant => GetValue(ThemeVariant.ActualThemeVariantProperty); /// @@ -322,24 +335,12 @@ namespace Avalonia /// IAvaloniaReadOnlyList IStyleable.Classes => Classes; - /// - /// Gets the type by which the styled element is styled. - /// - /// - /// Usually controls are styled by their own type, but there are instances where you want - /// a styled element to be styled by its base type, e.g. creating SpecialButton that - /// derives from Button and adds extra functionality but is still styled as a regular - /// Button. - /// - Type IStyleable.StyleKey => GetType(); - /// bool IStyleHost.IsStylesInitialized => _styles != null; /// IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; - /// public virtual void BeginInit() { @@ -682,7 +683,7 @@ namespace Avalonia // If the Theme property is not set, try to find a ControlTheme resource with our StyleKey. if (_implicitTheme is null) { - var key = ((IStyleable)this).StyleKey; + var key = GetStyleKey(this); if (this.TryFindResource(key, out var value) && value is ControlTheme t) _implicitTheme = t; @@ -713,6 +714,22 @@ namespace Avalonia } } + /// + /// Internal getter for so that we only need to suppress the obsolete + /// warning in one place. + /// + /// The element + /// + /// is obsolete and will be removed in a future version, but for backwards + /// compatibility we need to support code which overrides . + /// + internal static Type GetStyleKey(StyledElement e) + { +#pragma warning disable CS0618 // Type or member is obsolete + return ((IStyleable)e).StyleKey; +#pragma warning restore CS0618 // Type or member is obsolete + } + private static void DataContextNotifying(AvaloniaObject o, bool updateStarted) { if (o is StyledElement element) diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 22c8f61577..75a3beb907 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -46,7 +46,7 @@ namespace Avalonia.Styling if (TargetType is null) throw new InvalidOperationException("ControlTheme has no TargetType."); - if (HasSettersOrAnimations && TargetType.IsAssignableFrom(((IStyleable)target).StyleKey)) + if (HasSettersOrAnimations && TargetType.IsAssignableFrom(StyledElement.GetStyleKey(target))) { Attach(target, null, type); return SelectorMatchResult.AlwaysThisType; diff --git a/src/Avalonia.Base/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs index 77ae0f2877..20874a6877 100644 --- a/src/Avalonia.Base/Styling/DescendentSelector.cs +++ b/src/Avalonia.Base/Styling/DescendentSelector.cs @@ -44,9 +44,9 @@ namespace Avalonia.Styling { c = c.LogicalParent; - if (c is IStyleable) + if (c is StyledElement s) { - var match = _parent.Match((StyledElement)c, parent, subscribe); + var match = _parent.Match(s, parent, subscribe); if (match.Result == SelectorMatchResult.Sometimes) { diff --git a/src/Avalonia.Base/Styling/IStyleable.cs b/src/Avalonia.Base/Styling/IStyleable.cs index a6cc2c0f76..0768669905 100644 --- a/src/Avalonia.Base/Styling/IStyleable.cs +++ b/src/Avalonia.Base/Styling/IStyleable.cs @@ -1,13 +1,12 @@ using System; using Avalonia.Collections; -using Avalonia.Metadata; namespace Avalonia.Styling { /// /// Interface for styleable elements. /// - [NotClientImplementable] + [Obsolete("This interface may be removed in 12.0. Use StyledElement, or override StyledElement.StyleKeyOverride to override the StyleKey for a class.")] public interface IStyleable : INamed { /// @@ -18,6 +17,7 @@ namespace Avalonia.Styling /// /// Gets the type by which the control is styled. /// + [Obsolete("Override StyledElement.StyleKeyOverride instead.")] Type StyleKey { get; } /// diff --git a/src/Avalonia.Base/Styling/IThemeVariantHost.cs b/src/Avalonia.Base/Styling/IThemeVariantHost.cs index 01583148a8..740887970b 100644 --- a/src/Avalonia.Base/Styling/IThemeVariantHost.cs +++ b/src/Avalonia.Base/Styling/IThemeVariantHost.cs @@ -7,7 +7,6 @@ namespace Avalonia.Styling; /// /// Interface for the host element with a theme variant. /// -[Unstable] public interface IThemeVariantHost : IResourceHost { /// diff --git a/src/Avalonia.Base/Styling/NestingSelector.cs b/src/Avalonia.Base/Styling/NestingSelector.cs index 980bf12907..deb688ca4d 100644 --- a/src/Avalonia.Base/Styling/NestingSelector.cs +++ b/src/Avalonia.Base/Styling/NestingSelector.cs @@ -23,7 +23,7 @@ namespace Avalonia.Styling { if (theme.TargetType is null) throw new InvalidOperationException("ControlTheme has no TargetType."); - return theme.TargetType.IsAssignableFrom(((IStyleable)control).StyleKey) ? + return theme.TargetType.IsAssignableFrom(StyledElement.GetStyleKey(control)) ? SelectorMatch.AlwaysThisType : SelectorMatch.NeverThisType; } diff --git a/src/Avalonia.Base/Styling/Selectors.cs b/src/Avalonia.Base/Styling/Selectors.cs index 476d86cd11..d7406f2164 100644 --- a/src/Avalonia.Base/Styling/Selectors.cs +++ b/src/Avalonia.Base/Styling/Selectors.cs @@ -76,7 +76,7 @@ namespace Avalonia.Styling /// The type. /// The previous selector. /// The selector. - public static Selector Is(this Selector? previous) where T : IStyleable + public static Selector Is(this Selector? previous) where T : StyledElement { return previous.Is(typeof(T)); } @@ -171,7 +171,7 @@ namespace Avalonia.Styling /// The type. /// The previous selector. /// The selector. - public static Selector OfType(this Selector? previous) where T : IStyleable + public static Selector OfType(this Selector? previous) where T : StyledElement { return previous.OfType(typeof(T)); } diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs index 389136b0f5..23bc15dfa7 100644 --- a/src/Avalonia.Base/Styling/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -9,6 +9,10 @@ namespace Avalonia.Styling; /// Specifies a UI theme variant that should be used for the Control and Application types. /// [TypeConverter(typeof(ThemeVariantTypeConverter))] +[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1010:AvaloniaProperty objects should be owned by the type in which they are stored", + Justification = "ActualThemeVariant and RequestedThemeVariant properties are shared Avalonia.Base and Avalonia.Controls projects," + + "but shouldn't be visible on the StyledElement class." + + "Ideally we woould introduce readonly styled properties.")] public sealed record ThemeVariant { /// diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index a10b3eb3ea..2bd05242f5 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -93,7 +93,7 @@ namespace Avalonia.Styling { if (TargetType != null) { - var controlType = ((IStyleable)control).StyleKey ?? control.GetType(); + var controlType = StyledElement.GetStyleKey(control) ?? control.GetType(); if (IsConcreteType) { diff --git a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs index c6e9203f70..bb1663eac0 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Invoke.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -118,11 +117,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func synchronously on the + /// Executes the specified Func<TResult> synchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The return value from the delegate being invoked. @@ -136,11 +135,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func synchronously on the + /// Executes the specified Func<TResult> synchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -156,11 +155,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func synchronously on the + /// Executes the specified Func<TResult> synchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -183,11 +182,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func synchronously on the + /// Executes the specified Func<TResult> synchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -249,11 +248,11 @@ public partial class Dispatcher /// An operation representing the queued delegate to be invoked. /// /// - /// Note that the default priority is DispatcherPriority.Normal. + /// Note that the default priority is DispatcherPriority.Default. /// public DispatcherOperation InvokeAsync(Action callback) { - return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); + return InvokeAsync(callback, default, CancellationToken.None); } /// @@ -317,29 +316,29 @@ public partial class Dispatcher } /// - /// Executes the specified Func asynchronously on the + /// Executes the specified Func<TResult> asynchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// An operation representing the queued delegate to be invoked. /// /// - /// Note that the default priority is DispatcherPriority.Normal. + /// Note that the default priority is DispatcherPriority.Default. /// public DispatcherOperation InvokeAsync(Func callback) { - return InvokeAsync(callback, DispatcherPriority.Normal, CancellationToken.None); + return InvokeAsync(callback, DispatcherPriority.Default, CancellationToken.None); } /// - /// Executes the specified Func asynchronously on the + /// Executes the specified Func<TResult> asynchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -355,11 +354,11 @@ public partial class Dispatcher } /// - /// Executes the specified Func asynchronously on the + /// Executes the specified Func<TResult> asynchronously on the /// thread that the Dispatcher was created on. /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<TResult> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -479,7 +478,7 @@ public partial class Dispatcher // operation has already started when the timeout expires, // we still wait for it to complete. This is different // than simply waiting on the operation with a timeout - // because we are the ones queueing the dispatcher + // because we are the ones queuing the dispatcher // operation, not the caller. We can't leave the operation // in a state that it might execute if we return that it did not // invoke. @@ -492,12 +491,12 @@ public partial class Dispatcher // Old async semantics return from Wait without // throwing an exception if the operation was aborted. - // There is no need to test the timout condition, since + // There is no need to test the timeout condition, since // the old async semantics would just return the result, // which would be null. // This should not block because either the operation - // is using the old async sematics, or the operation + // is using the old async semantics, or the operation // completed successfully. result = operation.GetResult(); } @@ -543,11 +542,23 @@ public partial class Dispatcher } /// - /// Executes the specified Func asynchronously on the + /// Executes the specified Func<Task> asynchronously on the /// thread that the Dispatcher was created on /// /// - /// A Func delegate to invoke through the dispatcher. + /// A Func<Task> delegate to invoke through the dispatcher. + /// + /// + /// An task that completes after the task returned from callback finishes. + /// + public Task InvokeAsync(Func callback) => InvokeAsync(callback, DispatcherPriority.Default); + + /// + /// Executes the specified Func<Task> asynchronously on the + /// thread that the Dispatcher was created on + /// + /// + /// A Func<Task> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -557,18 +568,36 @@ public partial class Dispatcher /// /// An task that completes after the task returned from callback finishes /// - public Task InvokeTaskAsync(Func callback, DispatcherPriority priority = default) + public Task InvokeAsync(Func callback, DispatcherPriority priority) { _ = callback ?? throw new ArgumentNullException(nameof(callback)); - return InvokeAsync(callback, priority).GetTask().Unwrap(); + return InvokeAsync(callback, priority).GetTask().Unwrap(); } + + /// + /// Executes the specified Func<Task<TResult>> asynchronously on the + /// thread that the Dispatcher was created on + /// + /// + /// A Func<Task<TResult>> delegate to invoke through the dispatcher. + /// + /// + /// The priority that determines in what order the specified + /// callback is invoked relative to the other pending operations + /// in the Dispatcher. + /// + /// + /// An task that completes after the task returned from callback finishes + /// + public Task InvokeAsync(Func> action) => + InvokeAsync(action, DispatcherPriority.Default); /// - /// Executes the specified Func> asynchronously on the + /// Executes the specified Func<Task<TResult>> asynchronously on the /// thread that the Dispatcher was created on /// - /// - /// A Func> delegate to invoke through the dispatcher. + /// + /// A Func<Task<TResult>> delegate to invoke through the dispatcher. /// /// /// The priority that determines in what order the specified @@ -578,10 +607,10 @@ public partial class Dispatcher /// /// An task that completes after the task returned from callback finishes /// - public Task InvokeTaskAsync(Func> action, DispatcherPriority priority = default) + public Task InvokeAsync(Func> action, DispatcherPriority priority) { _ = action ?? throw new ArgumentNullException(nameof(action)); - return InvokeAsync(action, priority).GetTask().Unwrap(); + return InvokeAsync>(action, priority).GetTask().Unwrap(); } /// @@ -595,4 +624,4 @@ public partial class Dispatcher _ = action ?? throw new ArgumentNullException(nameof(action)); InvokeAsyncImpl(new SendOrPostCallbackDispatcherOperation(this, priority, action, arg, true), CancellationToken.None); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Threading/DispatcherFrame.cs b/src/Avalonia.Base/Threading/DispatcherFrame.cs index 1f8974dfa3..e826432475 100644 --- a/src/Avalonia.Base/Threading/DispatcherFrame.cs +++ b/src/Avalonia.Base/Threading/DispatcherFrame.cs @@ -91,31 +91,44 @@ public class DispatcherFrame internal void Run(IControlledDispatcherImpl impl) { - // Since the actual platform run loop is controlled by a Cancellation token, we are restarting - // it if frame still needs to run - while (Continue) - RunCore(impl); - } - - private void RunCore(IControlledDispatcherImpl impl) - { - if (_isRunning) - throw new InvalidOperationException("This frame is already running"); - _isRunning = true; - try - { - _cancellationTokenSource = new CancellationTokenSource(); - // Wake up the dispatcher in case it has pending jobs - Dispatcher.RequestProcessing(); - impl.RunLoop(_cancellationTokenSource.Token); - } - finally + Dispatcher.VerifyAccess(); + + // Since the actual platform run loop is controlled by a Cancellation token, we have an + // outer loop that restarts the platform one in case Continue was set to true after being set to false + while (true) { - _isRunning = false; - _cancellationTokenSource?.Cancel(); - _cancellationTokenSource = null; + // Take the instance lock since `Continue` is changed from one too + lock (Dispatcher.InstanceLock) + { + if (!Continue) + return; + + if (_isRunning) + throw new InvalidOperationException("This frame is already running"); + + _cancellationTokenSource = new CancellationTokenSource(); + _isRunning = true; + } + + try + { + // Wake up the dispatcher in case it has pending jobs + Dispatcher.RequestProcessing(); + impl.RunLoop(_cancellationTokenSource.Token); + } + finally + { + lock (Dispatcher.InstanceLock) + { + _isRunning = false; + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + } } } + internal void MaybeExitOnDispatcherRequest() { diff --git a/src/Avalonia.Base/Threading/DispatcherOperation.cs b/src/Avalonia.Base/Threading/DispatcherOperation.cs index 809c41ff02..8bd6d3bc01 100644 --- a/src/Avalonia.Base/Threading/DispatcherOperation.cs +++ b/src/Avalonia.Base/Threading/DispatcherOperation.cs @@ -331,6 +331,8 @@ public class DispatcherOperation : DispatcherOperation private TaskCompletionSource TaskCompletionSource => (TaskCompletionSource)TaskSource!; + public new TaskAwaiter GetAwaiter() => GetTask().GetAwaiter(); + public new Task GetTask() => TaskCompletionSource!.Task; protected override Task GetTaskCore() => GetTask(); diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index 3017b45dc7..a43dd8e4a2 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Avalonia.Threading { @@ -100,7 +101,7 @@ namespace Avalonia.Threading /// /// The job will be processed with the same priority as data binding. /// - [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = new(Layout); + [Obsolete("WPF compatibility"), EditorBrowsable(EditorBrowsableState.Never)] public static readonly DispatcherPriority DataBind = new(Layout); /// /// The job will be processed with normal priority. diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 4c30e2eb2c..ccbe3baf9a 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -6,7 +6,7 @@ using Avalonia.Platform; namespace Avalonia.Threading; -[Unstable] +[PrivateApi] public interface IDispatcherImpl { bool CurrentThreadIsLoopThread { get; } @@ -19,7 +19,7 @@ public interface IDispatcherImpl void UpdateTimer(long? dueTimeInMs); } -[Unstable] +[PrivateApi] public interface IDispatcherImplWithPendingInput : IDispatcherImpl { // Checks if dispatcher implementation can @@ -28,14 +28,14 @@ public interface IDispatcherImplWithPendingInput : IDispatcherImpl bool HasPendingInput { get; } } -[Unstable] +[PrivateApi] public interface IDispatcherImplWithExplicitBackgroundProcessing : IDispatcherImpl { event Action ReadyForBackgroundProcessing; void RequestBackgroundProcessing(); } -[Unstable] +[PrivateApi] public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput { // Runs the event loop diff --git a/src/Avalonia.Base/Utilities/SpanHelpers.cs b/src/Avalonia.Base/Utilities/SpanHelpers.cs index 9a5dce9798..f80ac7c046 100644 --- a/src/Avalonia.Base/Utilities/SpanHelpers.cs +++ b/src/Avalonia.Base/Utilities/SpanHelpers.cs @@ -4,7 +4,10 @@ using System.Runtime.CompilerServices; namespace Avalonia.Utilities { - public static class SpanHelpers +#if !BUILDTASK + public +#endif + static class SpanHelpers { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParseUInt(this ReadOnlySpan span, NumberStyles style, IFormatProvider provider, out uint value) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 05159eb4ae..30c89d186f 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -48,7 +48,7 @@ namespace Avalonia /// public static readonly StyledProperty ClipProperty = AvaloniaProperty.Register(nameof(Clip)); - + /// /// Defines the property. /// @@ -66,6 +66,12 @@ namespace Avalonia /// public static readonly StyledProperty OpacityMaskProperty = AvaloniaProperty.Register(nameof(OpacityMask)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty EffectProperty = + AvaloniaProperty.Register(nameof(Effect)); /// /// Defines the property. @@ -127,6 +133,8 @@ namespace Avalonia ClipToBoundsProperty, IsVisibleProperty, OpacityProperty, + OpacityMaskProperty, + EffectProperty, HasMirrorTransformProperty); RenderTransformProperty.Changed.Subscribe(RenderTransformChanged); ZIndexProperty.Changed.Subscribe(ZIndexChanged); @@ -233,6 +241,16 @@ namespace Avalonia get { return GetValue(OpacityMaskProperty); } set { SetValue(OpacityMaskProperty, value); } } + + /// + /// Gets or sets the effect of the control. + /// + public IEffect? Effect + { + get => GetValue(EffectProperty); + set => SetValue(EffectProperty, value); + } + /// /// Gets or sets a value indicating whether to apply mirror transform on this control. @@ -300,7 +318,9 @@ namespace Avalonia internal CompositionDrawListVisual? CompositionVisual { get; private set; } internal CompositionVisual? ChildCompositionVisual { get; set; } - + + internal RenderOptions RenderOptions { get; set; } + public bool HasNonUniformZIndexChildren { get; private set; } /// @@ -311,6 +331,7 @@ namespace Avalonia /// /// Gets the control's parent visual. /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1032", Justification = "GetVisualParent extension method is supposed to be used instead.")] internal Visual? VisualParent => _visualParent; /// diff --git a/src/Avalonia.Base/VisualTree/IVisualWithRoundRectClip.cs b/src/Avalonia.Base/VisualTree/IVisualWithRoundRectClip.cs index 9ace215d03..0079515a63 100644 --- a/src/Avalonia.Base/VisualTree/IVisualWithRoundRectClip.cs +++ b/src/Avalonia.Base/VisualTree/IVisualWithRoundRectClip.cs @@ -1,14 +1,15 @@ using System; +using System.ComponentModel; namespace Avalonia.VisualTree { - [Obsolete("Internal API, will be removed in future versions, you've been warned")] + [Obsolete("Internal API, will be removed in future versions, you've been warned"), EditorBrowsable(EditorBrowsableState.Never)] public interface IVisualWithRoundRectClip { /// /// Gets a value indicating the corner radius of control's clip bounds /// - [Obsolete("Internal API, will be removed in future versions, you've been warned")] + [Obsolete("Internal API, will be removed in future versions, you've been warned"), EditorBrowsable(EditorBrowsableState.Never)] CornerRadius ClipToBoundsRadius { get; } } diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 31722974ee..a24c249eed 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -6,7 +6,8 @@ Avalonia.Rendering.Composition.Animations - + + @@ -27,7 +28,9 @@ - + + + diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index e44b7290af..4f9c7416db 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -115,9 +115,10 @@ + - + diff --git a/src/Avalonia.Build.Tasks/SpanCompat.cs b/src/Avalonia.Build.Tasks/SpanCompat.cs index be59ff8b6c..00892d56e6 100644 --- a/src/Avalonia.Build.Tasks/SpanCompat.cs +++ b/src/Avalonia.Build.Tasks/SpanCompat.cs @@ -85,31 +85,7 @@ namespace System { return TrimStart().TrimEnd(); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseUInt(NumberStyles style, IFormatProvider provider, out uint value) - { - return uint.TryParse(ToString(), style, provider, out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseInt(out int value) - { - return int.TryParse(ToString(), out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseDouble(NumberStyles style, IFormatProvider provider, out double value) - { - return double.TryParse(ToString(), style, provider, out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseByte(NumberStyles style, IFormatProvider provider, out byte value) - { - return byte.TryParse(ToString(), style, provider, out value); - } - + public override string ToString() => _length == 0 ? string.Empty : _s.Substring(_start, _length); internal int IndexOf(ReadOnlySpan v, StringComparison ordinal, int start = 0) diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index a394d47904..d71070e818 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -149,7 +149,7 @@ namespace Avalonia.Build.Tasks { var ctor = asm.MainModule.ImportReference(typeSystem.GetTypeReference(asmMetadata).Resolve() .GetConstructors().First(c => c.Parameters.Count == 2).Resolve()); - var strType = asm.MainModule.ImportReference(typeof(string)); + var strType = asm.MainModule.TypeSystem.String; var arg1 = new CustomAttributeArgument(strType, "AvaloniaUseCompiledBindingsByDefault"); var arg2 = new CustomAttributeArgument(strType, defaultCompileBindings.ToString()); asm.CustomAttributes.Add(new CustomAttribute(ctor) { ConstructorArguments = { arg1, arg2 } }); diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 944e7b737a..a55a47fa53 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -152,8 +152,6 @@ namespace Avalonia.Controls private double _verticalOffset; private byte _verticalScrollChangesIgnored; - private IEnumerable _items; - public event EventHandler HorizontalScroll; public event EventHandler VerticalScroll; @@ -652,21 +650,18 @@ namespace Avalonia.Controls } /// - /// Identifies the ItemsSource dependency property. + /// Identifies the ItemsSource property. /// - public static readonly DirectProperty ItemsProperty = - AvaloniaProperty.RegisterDirect( - nameof(Items), - o => o.Items, - (o, v) => o.Items = v); + public static readonly StyledProperty ItemsSourceProperty = + AvaloniaProperty.Register(nameof(ItemsSource)); /// /// Gets or sets a collection that is used to generate the content of the control. /// - public IEnumerable Items + public IEnumerable ItemsSource { - get { return _items; } - set { SetAndRaise(ItemsProperty, ref _items, value); } + get => GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); } public static readonly StyledProperty AreRowDetailsFrozenProperty = @@ -713,7 +708,7 @@ namespace Avalonia.Controls HorizontalScrollBarVisibilityProperty, VerticalScrollBarVisibilityProperty); - ItemsProperty.Changed.AddClassHandler((x, e) => x.OnItemsPropertyChanged(e)); + ItemsSourceProperty.Changed.AddClassHandler((x, e) => x.OnItemsSourcePropertyChanged(e)); CanUserResizeColumnsProperty.Changed.AddClassHandler((x, e) => x.OnCanUserResizeColumnsChanged(e)); ColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthChanged(e)); FrozenColumnCountProperty.Changed.AddClassHandler((x, e) => x.OnFrozenColumnCountChanged(e)); @@ -734,6 +729,8 @@ namespace Avalonia.Controls RowDetailsTemplateProperty.Changed.AddClassHandler((x, e) => x.OnRowDetailsTemplateChanged(e)); RowDetailsVisibilityModeProperty.Changed.AddClassHandler((x, e) => x.OnRowDetailsVisibilityModeChanged(e)); AutoGenerateColumnsProperty.Changed.AddClassHandler((x, e) => x.OnAutoGenerateColumnsChanged(e)); + + FocusableProperty.OverrideDefaultValue(true); } /// @@ -816,10 +813,10 @@ namespace Avalonia.Controls } /// - /// ItemsProperty property changed handler. + /// ItemsSourceProperty property changed handler. /// /// The event arguments. - private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) + private void OnItemsSourcePropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (!_areHandlersSuspended) { @@ -830,7 +827,7 @@ namespace Avalonia.Controls if (LoadingOrUnloadingRow) { - SetValueNoCallback(ItemsProperty, oldValue); + SetValueNoCallback(ItemsSourceProperty, oldValue); throw DataGridError.DataGrid.CannotChangeItemsWhenLoadingRows(); } @@ -1855,7 +1852,7 @@ namespace Avalonia.Controls { get { - if (CurrentSlot == -1 || Items == null || RowGroupHeadersTable.Contains(CurrentSlot)) + if (CurrentSlot == -1 || ItemsSource == null || RowGroupHeadersTable.Contains(CurrentSlot)) { return null; } @@ -2483,7 +2480,7 @@ namespace Avalonia.Controls if (_hScrollBar != null) { - //_hScrollBar.IsTabStop = false; + _hScrollBar.IsTabStop = false; _hScrollBar.Maximum = 0.0; _hScrollBar.Orientation = Orientation.Horizontal; _hScrollBar.IsVisible = false; @@ -2499,7 +2496,7 @@ namespace Avalonia.Controls if (_vScrollBar != null) { - //_vScrollBar.IsTabStop = false; + _vScrollBar.IsTabStop = false; _vScrollBar.Maximum = 0.0; _vScrollBar.Orientation = Orientation.Vertical; _vScrollBar.IsVisible = false; @@ -3739,7 +3736,7 @@ namespace Avalonia.Controls if (sender is Control editingElement) { editingElement.LostFocus -= EditingElement_LostFocus; - if (EditingRow != null && EditingColumnIndex != -1) + if (EditingRow != null && _editingColumnIndex != -1) { FocusEditingCell(true); } @@ -3962,6 +3959,7 @@ namespace Avalonia.Controls bool focusLeftDataGrid = true; bool dataGridWillReceiveRoutedEvent = true; Visual focusedObject = FocusManager.Instance.Current as Visual; + DataGridColumn editingColumn = null; while (focusedObject != null) { @@ -3974,22 +3972,29 @@ namespace Avalonia.Controls // Walk up the visual tree. If we hit the root, try using the framework element's // parent. We do this because Popups behave differently with respect to the visual tree, // and it could have a parent even if the VisualTreeHelper doesn't find it. - Visual parent = focusedObject.GetVisualParent(); + var parent = focusedObject.Parent as Visual; if (parent == null) { - if (focusedObject is Control element) - { - parent = element.VisualParent; - if (parent != null) - { - dataGridWillReceiveRoutedEvent = false; - } - } + parent = focusedObject.GetVisualParent(); + } + else + { + dataGridWillReceiveRoutedEvent = false; } focusedObject = parent; } - if (focusLeftDataGrid) + if (EditingRow != null && EditingColumnIndex != -1) + { + editingColumn = ColumnsItemsInternal[EditingColumnIndex]; + + if (focusLeftDataGrid && editingColumn is DataGridTemplateColumn) + { + dataGridWillReceiveRoutedEvent = false; + } + } + + if (focusLeftDataGrid && !(editingColumn is DataGridTemplateColumn)) { ContainsFocus = false; if (EditingRow != null) @@ -4036,18 +4041,22 @@ namespace Avalonia.Controls return true; } - Debug.Assert(EditingRow != null); + var editingRow = EditingRow; + if (editingRow is null) + { + return true; + } + Debug.Assert(_editingColumnIndex >= 0); Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count); Debug.Assert(_editingColumnIndex == CurrentColumnIndex); - Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot); // Cache these to see if they change later int currentSlot = CurrentSlot; int currentColumnIndex = CurrentColumnIndex; // We're ready to start ending, so raise the event - DataGridCell editingCell = EditingRow.Cells[_editingColumnIndex]; + DataGridCell editingCell = editingRow.Cells[_editingColumnIndex]; var editingElement = editingCell.Content as Control; if (editingElement == null) { @@ -4055,7 +4064,7 @@ namespace Avalonia.Controls } if (raiseEvents) { - DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, EditingRow, editingElement, editAction); + DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, editingRow, editingElement, editAction); OnCellEditEnding(e); if (e.Cancel) { @@ -4109,7 +4118,7 @@ namespace Avalonia.Controls } else { - if (EditingRow != null) + if (editingRow != null) { if (editingCell.IsValid) { @@ -4117,10 +4126,10 @@ namespace Avalonia.Controls editingCell.UpdatePseudoClasses(); } - if (EditingRow.IsValid) + if (editingRow.IsValid) { - EditingRow.IsValid = false; - EditingRow.UpdatePseudoClasses(); + editingRow.IsValid = false; + editingRow.UpdatePseudoClasses(); } } @@ -4166,22 +4175,22 @@ namespace Avalonia.Controls PopulateCellContent( isCellEdited: !exitEditingMode, dataGridColumn: CurrentColumn, - dataGridRow: EditingRow, + dataGridRow: editingRow, dataGridCell: editingCell); - EditingRow.InvalidateDesiredHeight(); + editingRow.InvalidateDesiredHeight(); var column = editingCell.OwningColumn; if (column.Width.IsSizeToCells || column.Width.IsAuto) {// Invalidate desired width and force recalculation column.SetWidthDesiredValue(0); - EditingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width); + editingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width); } } // We're done, so raise the CellEditEnded event if (raiseEvents) { - OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, EditingRow, editAction)); + OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, editingRow, editAction)); } // There's a chance that somebody reopened this cell for edit within the CellEditEnded handler, @@ -4424,8 +4433,7 @@ namespace Avalonia.Controls dataGridCell.Focus(); success = dataGridCell.ContainsFocusedElement(); } - //TODO Check - //success = dataGridCell.ContainsFocusedElement() ? true : dataGridCell.Focus(); + _focusEditingControl = !success; } return success; diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 110590fef2..61a1eb2bf0 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -25,7 +25,7 @@ namespace Avalonia.Controls /// //TODO Binding [AssignBinding] - [InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))] + [InheritDataTypeFromItems(nameof(DataGrid.ItemsSource), AncestorType = typeof(DataGrid))] public virtual IBinding Binding { get diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index dd802678d4..599bea056b 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -33,6 +33,8 @@ namespace Avalonia.Controls { PointerPressedEvent.AddClassHandler( (x,e) => x.DataGridCell_PointerPressed(e), handledEventsToo: true); + FocusableProperty.OverrideDefaultValue(true); + IsTabStopProperty.OverrideDefaultValue(false); } public DataGridCell() { } @@ -169,8 +171,7 @@ namespace Avalonia.Controls OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e)); if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { - if (!e.Handled) - //if (!e.Handled && OwningGrid.IsTabStop) + if (!e.Handled && OwningGrid.IsTabStop) { OwningGrid.Focus(); } @@ -190,8 +191,7 @@ namespace Avalonia.Controls } else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) { - if (!e.Handled) - //if (!e.Handled && OwningGrid.IsTabStop) + if (!e.Handled && OwningGrid.IsTabStop) { OwningGrid.Focus(); } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 5250f80f77..ef1e84c745 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -72,6 +72,7 @@ namespace Avalonia.Controls { AreSeparatorsVisibleProperty.Changed.AddClassHandler((x, e) => x.OnAreSeparatorsVisibleChanged(e)); PressedMixin.Attach(); + IsTabStopProperty.OverrideDefaultValue(false); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs index 703bc0d9c3..4056b78bfe 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Reflection; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -489,7 +490,7 @@ namespace Avalonia.Controls { DataGridFillerColumn fillerColumn = ColumnsInternal.FillerColumn; double totalColumnsWidth = ColumnsInternal.VisibleEdgedColumnsWidth; - if (finalWidth > totalColumnsWidth) + if (finalWidth - totalColumnsWidth > LayoutHelper.LayoutEpsilon) { fillerColumn.FillerWidth = finalWidth - totalColumnsWidth; } @@ -971,6 +972,12 @@ namespace Avalonia.Controls { cx += _negHorizontalOffset; _horizontalOffset -= _negHorizontalOffset; + if (_horizontalOffset < LayoutHelper.LayoutEpsilon) + { + // Snap to zero to avoid trying to partially scroll in first scrolled off column below + _horizontalOffset = 0; + } + _negHorizontalOffset = 0; } else @@ -979,6 +986,11 @@ namespace Avalonia.Controls _negHorizontalOffset -= displayWidth - cx; cx = displayWidth; } + + // Make sure the HorizontalAdjustment is not greater than the new HorizontalOffset + // since it would cause an assertion failure in DataGridCellsPresenter.ShouldDisplayCell + // called by DataGridCellsPresenter.MeasureOverride. + HorizontalAdjustment = Math.Min(HorizontalAdjustment, _horizontalOffset); } // second try to scroll entire columns if (cx < displayWidth && _horizontalOffset > 0) diff --git a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs index ae52e5f970..ee9cc04420 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs @@ -122,9 +122,9 @@ namespace Avalonia.Controls // We need to use the raw ItemsSource as opposed to DataSource because DataSource // may be the ItemsSource wrapped in a collection view, in which case we wouldn't // be able to take T to be the type if we're given IEnumerable - if (_dataType == null && _owner.Items != null) + if (_dataType == null && _owner.ItemsSource != null) { - _dataType = _owner.Items.GetItemType(); + _dataType = _owner.ItemsSource.GetItemType(); } return _dataType; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index ea9b2fe972..dfda7d6e4f 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -128,6 +128,7 @@ namespace Avalonia.Controls DetailsTemplateProperty.Changed.AddClassHandler((x, e) => x.OnDetailsTemplateChanged(e)); AreDetailsVisibleProperty.Changed.AddClassHandler((x, e) => x.OnAreDetailsVisibleChanged(e)); PointerPressedEvent.AddClassHandler((x, e) => x.DataGridRow_PointerPressed(e), handledEventsToo: true); + IsTabStopProperty.OverrideDefaultValue(false); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 10efded58a..e51c2526b1 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -106,6 +106,7 @@ namespace Avalonia.Controls { SublevelIndentProperty.Changed.AddClassHandler((x,e) => x.OnSublevelIndentChanged(e)); PressedMixin.Attach(); + IsTabStopProperty.OverrideDefaultValue(false); } /// @@ -301,8 +302,7 @@ namespace Avalonia.Controls } else { - //if (!e.Handled && OwningGrid.IsTabStop) - if (!e.Handled) + if (!e.Handled && OwningGrid.IsTabStop) { OwningGrid.Focus(); } diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index 00e035270c..44079d24d0 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -1589,6 +1589,23 @@ namespace Avalonia.Controls CorrectSlotsAfterDeletion(slot, isRow); OnRemovedElement(slot, item); + + // Synchronize CurrentCellCoordinates, CurrentColumn, CurrentColumnIndex, CurrentItem + // and CurrentSlot with the currently edited cell, since OnRemovingElement called + // SetCurrentCellCore(-1, -1) to temporarily reset the current cell. + if (_temporarilyResetCurrentCell && + _editingColumnIndex != -1 && + _previousCurrentItem != null && + EditingRow != null && + EditingRow.Slot != -1) + { + ProcessSelectionAndCurrency( + columnIndex: _editingColumnIndex, + item: _previousCurrentItem, + backupSlot: this.EditingRow.Slot, + action: DataGridSelectionAction.None, + scrollIntoView: false); + } } private void RemoveNonDisplayedRows(int newFirstDisplayedSlot, int newLastDisplayedSlot) diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 00318e2dd8..0bfb4b6913 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls (o, v) => o.CellTemplate = v); [Content] - [InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))] + [InheritDataTypeFromItems(nameof(DataGrid.ItemsSource), AncestorType = typeof(DataGrid))] public IDataTemplate CellTemplate { get { return _cellTemplate; } @@ -51,7 +51,7 @@ namespace Avalonia.Controls /// /// If this property is the column is read-only. /// - [InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))] + [InheritDataTypeFromItems(nameof(DataGrid.ItemsSource), AncestorType = typeof(DataGrid))] public IDataTemplate CellEditingTemplate { get => _cellEditingCellTemplate; diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index e4642c1453..082eac60be 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -82,7 +82,6 @@ - - @@ -268,7 +266,6 @@ - @@ -310,7 +307,6 @@ - @@ -408,7 +404,6 @@ - @@ -433,7 +428,7 @@ BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="{TemplateBinding CornerRadius}" - Focusable="False" + IsTabStop="False" Foreground="{TemplateBinding Foreground}" /> +