diff --git a/.github/pr_labels.yml b/.github/pr_labels.yml new file mode 100644 index 0000000000..2b0eb795be --- /dev/null +++ b/.github/pr_labels.yml @@ -0,0 +1,8 @@ +version: '1.1.0' +invalidStatus: "pending" +labelRule: + values: + - "bug" + - "feature" + - "enhancement" + - "area-infrastructure" diff --git a/.gitmodules b/.gitmodules index 032bc879cc..2d11fdfa9e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github"] path = src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github url = https://github.com/kekekeks/XamlX.git -[submodule "nukebuild/il-repack"] - path = nukebuild/il-repack - url = https://github.com/Gillibald/il-repack diff --git a/Avalonia.sln b/Avalonia.sln index 2ebd2550ae..c995888350 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -116,19 +116,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\SourceGenerators.props = build\SourceGenerators.props build\SourceLink.props = build\SourceLink.props build\System.Memory.props = build\System.Memory.props + build\TargetFrameworks.props = build\TargetFrameworks.props build\TrimmingEnable.props = build\TrimmingEnable.props build\UnitTests.NetFX.props = build\UnitTests.NetFX.props - build\XUnit.props = build\XUnit.props - build\TargetFrameworks.props = build\TargetFrameworks.props build\WarnAsErrors.props = build\WarnAsErrors.props + build\XUnit.props = build\XUnit.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}" ProjectSection(SolutionItems) = preProject build\BuildTargets.targets = build\BuildTargets.targets + build\DevSingleProject.targets = build\DevSingleProject.targets build\LegacyProject.targets = build\LegacyProject.targets build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets - build\DevSingleProject.targets = build\DevSingleProject.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}" @@ -232,8 +232,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - azure-pipelines.yml = azure-pipelines.yml azure-pipelines-integrationtests.yml = azure-pipelines-integrationtests.yml + azure-pipelines.yml = azure-pipelines.yml CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md CONTRIBUTING.md = CONTRIBUTING.md Directory.Build.props = Directory.Build.props @@ -283,24 +283,23 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Tizen", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Metal", "src\Avalonia.Metal\Avalonia.Metal.csproj", "{60B4ED1F-ECFA-453B-8A70-1788261C8355}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Build.Tasks.UnitTest", "tests\Avalonia.Build.Tasks.UnitTest\Avalonia.Build.Tasks.UnitTest.csproj", "{B0FD6A48-FBAB-4676-B36A-DE76B0922B12}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Build.Tasks.UnitTest", "tests\Avalonia.Build.Tasks.UnitTest\Avalonia.Build.Tasks.UnitTest.csproj", "{B0FD6A48-FBAB-4676-B36A-DE76B0922B12}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestFiles", "TestFiles", "{9D6AEF22-221F-4F4B-B335-A4BA510F002C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildTasks", "BuildTasks", "{5BF0C3B8-E595-4940-AB30-2DA206C2F085}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PInvoke", "tests\TestFiles\BuildTasks\PInvoke\PInvoke.csproj", "{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PInvoke", "tests\TestFiles\BuildTasks\PInvoke\PInvoke.csproj", "{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}" EndProject - Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnloadableAssemblyLoadContext", "UnloadableAssemblyLoadContext", "{9CCA131B-DE95-4D44-8788-C3CAE28574CD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnloadableAssemblyLoadContext", "samples\UnloadableAssemblyLoadContext\UnloadableAssemblyLoadContext\UnloadableAssemblyLoadContext.csproj", "{D7FE3E0F-3FE0-4F87-A2F5-24F1454D84C0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnloadableAssemblyLoadContext", "samples\UnloadableAssemblyLoadContext\UnloadableAssemblyLoadContext\UnloadableAssemblyLoadContext.csproj", "{D7FE3E0F-3FE0-4F87-A2F5-24F1454D84C0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnloadableAssemblyLoadContextPlug", "samples\UnloadableAssemblyLoadContext\UnloadableAssemblyLoadContextPlug\UnloadableAssemblyLoadContextPlug.csproj", "{DA5F1FF9-4259-4C54-B443-85CFA226EE6A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnloadableAssemblyLoadContextPlug", "samples\UnloadableAssemblyLoadContext\UnloadableAssemblyLoadContextPlug\UnloadableAssemblyLoadContextPlug.csproj", "{DA5F1FF9-4259-4C54-B443-85CFA226EE6A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Vulkan", "src\Avalonia.Vulkan\Avalonia.Vulkan.csproj", "{3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Vulkan", "src\Avalonia.Vulkan\Avalonia.Vulkan.csproj", "{3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.RenderTests.WpfCompare", "tests\Avalonia.RenderTests.WpfCompare\Avalonia.RenderTests.WpfCompare.csproj", "{9AE1B827-21AC-4063-AB22-C8804B7F931E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.RenderTests.WpfCompare", "tests\Avalonia.RenderTests.WpfCompare\Avalonia.RenderTests.WpfCompare.csproj", "{9AE1B827-21AC-4063-AB22-C8804B7F931E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -678,10 +677,6 @@ Global {60B4ED1F-ECFA-453B-8A70-1788261C8355}.Debug|Any CPU.Build.0 = Debug|Any CPU {60B4ED1F-ECFA-453B-8A70-1788261C8355}.Release|Any CPU.ActiveCfg = Release|Any CPU {60B4ED1F-ECFA-453B-8A70-1788261C8355}.Release|Any CPU.Build.0 = Release|Any CPU - {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Release|Any CPU.Build.0 = Release|Any CPU {B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Debug|Any CPU.Build.0 = Debug|Any CPU {B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -698,6 +693,10 @@ Global {DA5F1FF9-4259-4C54-B443-85CFA226EE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU {DA5F1FF9-4259-4C54-B443-85CFA226EE6A}.Release|Any CPU.ActiveCfg = Release|Any CPU {DA5F1FF9-4259-4C54-B443-85CFA226EE6A}.Release|Any CPU.Build.0 = Release|Any CPU + {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Release|Any CPU.Build.0 = Release|Any CPU {9AE1B827-21AC-4063-AB22-C8804B7F931E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9AE1B827-21AC-4063-AB22-C8804B7F931E}.Debug|Any CPU.Build.0 = Debug|Any CPU {9AE1B827-21AC-4063-AB22-C8804B7F931E}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings index cc58566622..061db72168 100644 --- a/Avalonia.sln.DotSettings +++ b/Avalonia.sln.DotSettings @@ -36,7 +36,20 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="False" Prefix="T" Suffix="" Style="AaBb" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Type parameters"><ElementKinds><Kind Name="TYPE_PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="False" Prefix="T" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local variables"><ElementKinds><Kind Name="LOCAL_VARIABLE" /></ElementKinds></Descriptor><Policy Inspect="False" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Parameters"><ElementKinds><Kind Name="PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="False" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Enum members"><ElementKinds><Kind Name="ENUM_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Interfaces"><ElementKinds><Kind Name="INTERFACE" /></ElementKinds></Descriptor><Policy Inspect="False" Prefix="I" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static readonly fields (not private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /></Policy> True + True True True True diff --git a/Documentation/build.md b/Documentation/build.md index ea91f035f8..065d2ee960 100644 --- a/Documentation/build.md +++ b/Documentation/build.md @@ -100,7 +100,14 @@ On macOS it is necessary to build and manually install the respective native lib # Building Avalonia into a local NuGet cache It is possible to build Avalonia locally and generate NuGet packages that can be used locally to test local changes. -To do so you need to run: + +First, install Nuke's dotnet global tool like so: + +```bash +dotnet tool install Nuke.GlobalTool --global +``` + +Then you need to run: ```bash nuke --target BuildToNuGetCache --configuration Release ``` diff --git a/api/Avalonia.Android.nupkg.xml b/api/Avalonia.Android.nupkg.xml index 5e68aafc3f..da33e03f2c 100644 --- a/api/Avalonia.Android.nupkg.xml +++ b/api/Avalonia.Android.nupkg.xml @@ -2,93 +2,15 @@ - CP0001 - T:Avalonia.Android.Internal.Resource.Animation - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Animator - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Attribute - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Boolean - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Color - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Dimension - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Drawable - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Id - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Integer - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Interpolator - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Layout - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.String - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Style - baseline/net6.0-android31.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0001 - T:Avalonia.Android.Internal.Resource.Styleable - baseline/net6.0-android31.0/Avalonia.Android.dll + CP0002 + M:Avalonia.Android.AndroidViewControlHandle.get_HandleDescriptor + baseline/net8.0-android34.0/Avalonia.Android.dll target/net8.0-android34.0/Avalonia.Android.dll CP0007 - T:Avalonia.Android.Internal.Resource - baseline/net6.0-android31.0/Avalonia.Android.dll + T:Avalonia.Android.AndroidViewControlHandle + baseline/net8.0-android34.0/Avalonia.Android.dll target/net8.0-android34.0/Avalonia.Android.dll \ No newline at end of file diff --git a/api/Avalonia.Browser.nupkg.xml b/api/Avalonia.Browser.nupkg.xml index 16bdbfe25d..0fb414ed14 100644 --- a/api/Avalonia.Browser.nupkg.xml +++ b/api/Avalonia.Browser.nupkg.xml @@ -3,20 +3,20 @@ CP0002 - M:Avalonia.Browser.AvaloniaView.get_IsComposing - baseline/net7.0/Avalonia.Browser.dll + M:Avalonia.Browser.JSObjectControlHandle.get_Handle + baseline/net8.0-browser1.0/Avalonia.Browser.dll target/net8.0-browser1.0/Avalonia.Browser.dll CP0002 - M:Avalonia.Browser.AvaloniaView.OnDragEvent(System.Runtime.InteropServices.JavaScript.JSObject) - baseline/net7.0/Avalonia.Browser.dll + M:Avalonia.Browser.JSObjectControlHandle.get_HandleDescriptor + baseline/net8.0-browser1.0/Avalonia.Browser.dll target/net8.0-browser1.0/Avalonia.Browser.dll - CP0008 - T:Avalonia.Browser.AvaloniaView - baseline/net7.0/Avalonia.Browser.dll + CP0007 + T:Avalonia.Browser.JSObjectControlHandle + baseline/net8.0-browser1.0/Avalonia.Browser.dll target/net8.0-browser1.0/Avalonia.Browser.dll \ No newline at end of file diff --git a/api/Avalonia.Controls.ColorPicker.nupkg.xml b/api/Avalonia.Controls.ColorPicker.nupkg.xml deleted file mode 100644 index 8fadd656ea..0000000000 --- a/api/Avalonia.Controls.ColorPicker.nupkg.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Fluent/ColorPicker.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Fluent/ColorPreviewer.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Fluent/ColorSlider.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Fluent/ColorSpectrum.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Fluent/ColorView.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Fluent/Fluent.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Simple/ColorPicker.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Simple/ColorPreviewer.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Simple/ColorSlider.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Simple/ColorSpectrum.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Simple/ColorView.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Simple/Simple.xaml - baseline/netstandard2.0/Avalonia.Controls.ColorPicker.dll - target/netstandard2.0/Avalonia.Controls.ColorPicker.dll - - \ No newline at end of file diff --git a/api/Avalonia.Controls.DataGrid.nupkg.xml b/api/Avalonia.Controls.DataGrid.nupkg.xml deleted file mode 100644 index 1dc020aa79..0000000000 --- a/api/Avalonia.Controls.DataGrid.nupkg.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Fluent.xaml - baseline/netstandard2.0/Avalonia.Controls.DataGrid.dll - target/netstandard2.0/Avalonia.Controls.DataGrid.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Themes/Simple.xaml - baseline/netstandard2.0/Avalonia.Controls.DataGrid.dll - target/netstandard2.0/Avalonia.Controls.DataGrid.dll - - \ No newline at end of file diff --git a/api/Avalonia.Diagnostics.nupkg.xml b/api/Avalonia.Diagnostics.nupkg.xml deleted file mode 100644 index 5bc2bfa99c..0000000000 --- a/api/Avalonia.Diagnostics.nupkg.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Diagnostics/Controls/BrushEditor.axaml - baseline/netstandard2.0/Avalonia.Diagnostics.dll - target/netstandard2.0/Avalonia.Diagnostics.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Diagnostics/Controls/FilterTextBox.axaml - baseline/netstandard2.0/Avalonia.Diagnostics.dll - target/netstandard2.0/Avalonia.Diagnostics.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Diagnostics/Controls/ThicknessEditor.axaml - baseline/netstandard2.0/Avalonia.Diagnostics.dll - target/netstandard2.0/Avalonia.Diagnostics.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Diagnostics/Views/ConsoleView.xaml - baseline/netstandard2.0/Avalonia.Diagnostics.dll - target/netstandard2.0/Avalonia.Diagnostics.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Diagnostics/Views/ControlDetailsView.xaml - baseline/netstandard2.0/Avalonia.Diagnostics.dll - target/netstandard2.0/Avalonia.Diagnostics.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Diagnostics/Views/EventsPageView.xaml - baseline/netstandard2.0/Avalonia.Diagnostics.dll - target/netstandard2.0/Avalonia.Diagnostics.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Diagnostics/Views/LayoutExplorerView.axaml - baseline/netstandard2.0/Avalonia.Diagnostics.dll - target/netstandard2.0/Avalonia.Diagnostics.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Diagnostics/Views/MainView.xaml - baseline/netstandard2.0/Avalonia.Diagnostics.dll - target/netstandard2.0/Avalonia.Diagnostics.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Diagnostics/Views/MainWindow.xaml - baseline/netstandard2.0/Avalonia.Diagnostics.dll - target/netstandard2.0/Avalonia.Diagnostics.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Diagnostics/Views/TreePageView.xaml - baseline/netstandard2.0/Avalonia.Diagnostics.dll - target/netstandard2.0/Avalonia.Diagnostics.dll - - \ No newline at end of file diff --git a/api/Avalonia.FreeDesktop.nupkg.xml b/api/Avalonia.FreeDesktop.nupkg.xml new file mode 100644 index 0000000000..f5fcb60bc8 --- /dev/null +++ b/api/Avalonia.FreeDesktop.nupkg.xml @@ -0,0 +1,10 @@ + + + + + CP0001 + T:Tmds.DBus.SourceGenerator.PropertyChanges`1 + baseline/netstandard2.0/Avalonia.FreeDesktop.dll + target/netstandard2.0/Avalonia.FreeDesktop.dll + + \ No newline at end of file diff --git a/api/Avalonia.Skia.nupkg.xml b/api/Avalonia.Skia.nupkg.xml index a88fc8ba0a..7abbc47cea 100644 --- a/api/Avalonia.Skia.nupkg.xml +++ b/api/Avalonia.Skia.nupkg.xml @@ -1,10 +1,10 @@ - + CP0006 - M:Avalonia.Skia.ISkiaSharpApiLease.TryLeasePlatformGraphicsApi + M:Avalonia.Skia.ISkiaGpuWithPlatformGraphicsContext.TryGetGrContext baseline/netstandard2.0/Avalonia.Skia.dll target/netstandard2.0/Avalonia.Skia.dll - \ No newline at end of file + diff --git a/api/Avalonia.Themes.Fluent.nupkg.xml b/api/Avalonia.Themes.Fluent.nupkg.xml deleted file mode 100644 index 08fb493760..0000000000 --- a/api/Avalonia.Themes.Fluent.nupkg.xml +++ /dev/null @@ -1,448 +0,0 @@ - - - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Accents/BaseColorsPalette.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Accents/BaseResources.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Accents/FluentControlResources.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/AdornerLayer.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/AutoCompleteBox.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Button.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ButtonSpinner.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Calendar.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CalendarButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CalendarDatePicker.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CalendarDayButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CalendarItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CaptionButtons.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Carousel.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CheckBox.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ComboBox.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ComboBoxItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ContentControl.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ContextMenu.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/DataValidationErrors.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/DatePicker.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/DateTimePickerShared.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/DropDownButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/EmbeddableControlRoot.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Expander.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/FluentControls.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/FlyoutPresenter.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/GridSplitter.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ItemsControl.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Label.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ListBox.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ListBoxItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ManagedFileChooser.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Menu.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/MenuFlyoutPresenter.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/MenuItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/MenuScrollViewer.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/NativeMenuBar.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/NotificationCard.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/NumericUpDown.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/OverlayPopupHost.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/PathIcon.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/PopupRoot.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ProgressBar.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/RadioButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/RefreshContainer.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/RefreshVisualizer.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/RepeatButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ScrollBar.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ScrollViewer.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/SelectableTextBlock.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Separator.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Slider.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/SplitButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/SplitView.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TabControl.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TabItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TabStrip.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TabStripItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TextBox.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ThemeVariantScope.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TimePicker.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TitleBar.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ToggleButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ToggleSwitch.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ToolTip.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TransitioningContentControl.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TreeView.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TreeViewItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/UserControl.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Window.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/WindowNotificationManager.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/DensityStyles/Compact.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/FluentTheme.xaml - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - \ No newline at end of file diff --git a/api/Avalonia.Themes.Simple.nupkg.xml b/api/Avalonia.Themes.Simple.nupkg.xml deleted file mode 100644 index 8f4ac0585a..0000000000 --- a/api/Avalonia.Themes.Simple.nupkg.xml +++ /dev/null @@ -1,424 +0,0 @@ - - - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Accents/Base.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/AdornerLayer.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/AutoCompleteBox.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Button.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ButtonSpinner.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Calendar.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CalendarButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CalendarDatePicker.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CalendarDayButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CalendarItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CaptionButtons.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Carousel.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/CheckBox.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ComboBox.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ComboBoxItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ContentControl.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ContextMenu.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/DataValidationErrors.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/DatePicker.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/DateTimePickerShared.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/DropDownButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/EmbeddableControlRoot.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Expander.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/FlyoutPresenter.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/GridSplitter.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ItemsControl.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Label.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ListBox.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ListBoxItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ManagedFileChooser.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Menu.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/MenuFlyoutPresenter.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/MenuItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/NativeMenuBar.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/NotificationCard.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/NumericUpDown.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/OverlayPopupHost.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/PathIcon.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/PopupRoot.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ProgressBar.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/RadioButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/RefreshContainer.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/RefreshVisualizer.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/RepeatButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ScrollBar.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ScrollViewer.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/SelectableTextBlock.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Separator.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/SimpleControls.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Slider.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/SplitButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/SplitView.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TabControl.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TabItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TabStrip.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TabStripItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TextBox.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ThemeVariantScope.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TimePicker.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TitleBar.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ToggleButton.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ToggleSwitch.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/ToolTip.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TransitioningContentControl.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TreeView.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/TreeViewItem.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/UserControl.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/Window.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/Controls/WindowNotificationManager.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/SimpleTheme.xaml - baseline/netstandard2.0/Avalonia.Themes.Simple.dll - target/netstandard2.0/Avalonia.Themes.Simple.dll - - \ No newline at end of file diff --git a/api/Avalonia.iOS.nupkg.xml b/api/Avalonia.iOS.nupkg.xml new file mode 100644 index 0000000000..5f6e822d81 --- /dev/null +++ b/api/Avalonia.iOS.nupkg.xml @@ -0,0 +1,16 @@ + + + + + CP0002 + M:Avalonia.iOS.UIViewControlHandle.get_HandleDescriptor + baseline/net8.0-tvos17.0/Avalonia.iOS.dll + target/net8.0-tvos17.0/Avalonia.iOS.dll + + + CP0007 + T:Avalonia.iOS.UIViewControlHandle + baseline/net8.0-tvos17.0/Avalonia.iOS.dll + target/net8.0-tvos17.0/Avalonia.iOS.dll + + \ No newline at end of file diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 00ec23467d..6b79f6f33b 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -3,1106 +3,50 @@ CP0001 - T:XamlX.Ast.IXamlAstImperativeNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.IXamlAstManipulationNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.IXamlAstNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.IXamlAstNodeNeedsParentStack - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.IXamlAstPropertyReference - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.IXamlAstTypeReference - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.IXamlAstValueNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.IXamlAstVisitor - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.IXamlILOptimizedEmitablePropertySetter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.IXamlLineInfo - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.IXamlPropertySetter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.IXamlWrappedMethod - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.PropertySetterBinderParameters - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstClrProperty - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstClrTypeReference - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstCompilerLocalNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstConstructableObjectNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstContextLocalNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstExtensions - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstImperativeValueManipulation - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstLocalInitializationNodeEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstManipulationImperativeNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstNamePropertyReference - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstNeedsParentStackValueNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstNewClrObjectNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstObjectNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstRuntimeCastNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstTextNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstXamlPropertyValueNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstXmlDirective - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlAstXmlTypeReference - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlConstantNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlDeferredContentInitializeIntermediateRootNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlDeferredContentNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlDocument - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlIntermediateRootObjectNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlLoadMethodDelegateNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlManipulationGroupNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlMarkupExtensionNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlMethodCallBaseNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlMethodWithCasts - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlNoReturnMethodCallNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlNullExtensionNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlObjectInitializationNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlPropertyAssignmentNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlPropertyValueManipulationNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlRootObjectNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlStaticExtensionNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlStaticOrTargetedReturnMethodCallNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlToArrayNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlTypeExtensionNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlValueNodeWithBeginInit - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlValueWithManipulationNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlValueWithSideEffectNodeBase - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlWrappedMethod - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Ast.XamlWrappedMethodWithCasts - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Compiler.XamlCompiler`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Compiler.XamlImperativeCompiler`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IHasLocalsPool - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlAstEmitableNode`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlAstLocalsEmitableNode`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlAstLocalsNodeEmitter`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlAstNodeEmitter`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlCustomEmitMethod`1 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlCustomEmitMethodWithContext`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlEmitablePropertySetter`1 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlEmitableWrappedMethod`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlEmitableWrappedMethodWithLocals`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlEmitResult - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlPropertySetterEmitter`1 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlWrappedMethodEmitter`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.IXamlWrappedMethodEmitterWithLocals`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.XamlContextFactoryCallback`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.XamlContextTypeBuilderCallback`1 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.XamlEmitContext`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.XamlEmitContextWithLocals`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.XamlLanguageEmitMappings`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Emit.XamlRuntimeContext`2 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.CheckingILEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.Emitters.ManipulationGroupEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.Emitters.MarkupExtensionEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.Emitters.MethodCallEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.Emitters.NewObjectEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.Emitters.ObjectInitializationNodeEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.Emitters.PropertyAssignmentEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.Emitters.PropertyValueManipulationEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.Emitters.TextNodeEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.Emitters.ValueWithManipulationsEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.ILEmitContext - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.ILEmitContextSettings - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.ILEmitHelpers - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.IXamlAstILEmitableNode - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.IXamlILAstNodeEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.IXamlILEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.IXamlILLocal - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.NamespaceInfoProvider - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.RecordingIlEmitter - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.RuntimeContext - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.SreTypeSystem - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.XamlILCompiler - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.XamlILContextDefinition - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.XamlIlEmitterExtensions - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.IL.XamlILNodeEmitResult - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Parsers.CommaSeparatedParenthesesTreeParser - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Parsers.SystemXamlMarkupExtensionParser.SystemXamlMarkupExtensionParser - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Parsers.XamlMarkupExtensionParser - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Parsers.XDocumentXamlParser - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Parsers.XDocumentXamlParserSettings - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.AstTransformationContext - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.GuidIdentifierGenerator - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.IXamlAstTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.IXamlCustomAttributeResolver - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.IXamlIdentifierGenerator - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.NamespaceInfoHelper - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.TransformerConfiguration - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.ApplyWhitespaceNormalization - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.ConstructableObjectTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.ContentConvertTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.ConvertPropertyValuesToAssignmentsTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.DeferredContentTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.FlattenAstTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.KnownDirectivesTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.MarkupExtensionTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.NewObjectTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.PropertyReferenceResolver - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.RemoveWhitespaceBetweenPropertyValuesTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.ResolveContentPropertyTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.ResolvePropertyValueAddersTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.TextNodeMerger - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.TopDownInitializationTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.TypeReferenceResolver - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.XamlIntrinsicsTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.Transformers.XArgumentsTransformer - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.XamlContextBase - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.XamlLanguageTypeMappings - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.XamlTransformHelpers - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.XamlTypeWellKnownTypes - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.Transform.XamlXmlnsMappings - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.FindMethodMethodSignature - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IFileSource - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlAssembly - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlConstructor - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlConstructorBuilder`1 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlCustomAttribute - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlDelegateTypeBuilder - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlEventInfo - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlField - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlLabel - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlLocal - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlMember - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlMethod - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlMethodBuilder`1 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlProperty - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlType - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlTypeBuilder`1 - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.IXamlTypeSystem - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.TypeSystemHelpers - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.XamlGenericParameterConstraint - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.XamlLocalsPool - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.XamlPseudoType - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.TypeSystem.XamlTypeSystemExtensions - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.XamlLoadException - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.XamlNamespaces - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.XamlParseException - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.XamlTransformException - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:XamlX.XamlTypeSystemException - baseline/designer/Avalonia.Designer.HostApp.dll - target/designer/Avalonia.Designer.HostApp.dll - - - CP0001 - T:Avalonia.AvaloniaLocator.RegistrationHelper`1 - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.AvaloniaLocator.ResolverDisposable - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Input.FocusManager.<GetFocusScopeAncestors>d__18 + T:Avalonia.Diagnostics.AppliedStyle baseline/netstandard2.0/Avalonia.Base.dll target/netstandard2.0/Avalonia.Base.dll CP0001 - T:Avalonia.Layout.LayoutManager.<>c + T:Avalonia.Diagnostics.StyleDiagnostics baseline/netstandard2.0/Avalonia.Base.dll target/netstandard2.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Layout.LayoutManager.ArrangeResult - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Layout.LayoutManager.EffectiveViewportChangedListener - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Rendering.DefaultRenderTimer.<>c__DisplayClass13_0 - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Rendering.UiThreadRenderTimer.<>c__DisplayClass3_0 + CP0002 + M:Avalonia.Diagnostics.StyledElementExtensions.GetStyleDiagnostics(Avalonia.StyledElement) baseline/netstandard2.0/Avalonia.Base.dll target/netstandard2.0/Avalonia.Base.dll - CP0001 - T:Avalonia.Controls.Primitives.PopupPositioning.ManagedPopupPositioner.<>c__DisplayClass5_0 + CP0002 + M:Avalonia.Controls.Screens.#ctor(Avalonia.Platform.IScreenImpl) baseline/netstandard2.0/Avalonia.Controls.dll target/netstandard2.0/Avalonia.Controls.dll - CP0001 - T:Avalonia.Controls.Primitives.PopupPositioning.ManagedPopupPositionerPopupImplHelper.<>c + CP0006 + M:Avalonia.Controls.Notifications.IManagedNotificationManager.Close(System.Object) baseline/netstandard2.0/Avalonia.Controls.dll target/netstandard2.0/Avalonia.Controls.dll - CP0001 - T:Avalonia.Controls.Primitives.PopupPositioning.ManagedPopupPositionerPopupImplHelper.MoveResizeDelegate + CP0006 + M:Avalonia.Controls.Notifications.INotificationManager.Close(Avalonia.Controls.Notifications.INotification) baseline/netstandard2.0/Avalonia.Controls.dll target/netstandard2.0/Avalonia.Controls.dll - - CP0001 - T:CompiledAvaloniaXaml.!AvaloniaResources.NamespaceInfo:/AboutAvaloniaDialog.xaml - baseline/netstandard2.0/Avalonia.Dialogs.dll - target/netstandard2.0/Avalonia.Dialogs.dll - - - CP0006 - M:Avalonia.Platform.IAssetLoader.InvalidateAssemblyCache - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0006 - M:Avalonia.Platform.IAssetLoader.InvalidateAssemblyCache(System.String) - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0006 - P:Avalonia.Media.IRadialGradientBrush.RadiusX - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - CP0006 - P:Avalonia.Media.IRadialGradientBrush.RadiusY - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll + M:Avalonia.Controls.Notifications.INotificationManager.CloseAll + baseline/netstandard2.0/Avalonia.Controls.dll + target/netstandard2.0/Avalonia.Controls.dll - CP0006 - P:Avalonia.Rendering.Composition.ICompositionGpuImportedObject.ImportCompleted - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll + CP0009 + T:Avalonia.Controls.Screens + baseline/netstandard2.0/Avalonia.Controls.dll + target/netstandard2.0/Avalonia.Controls.dll \ No newline at end of file diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index ce9b38c44d..2c3a2abc25 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -83,11 +83,23 @@ jobs: projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj' - task: DotNetCoreCLI@2 + displayName: 'Build test project' + inputs: + command: 'build' + projects: 'tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj' + + - task: VSTest@2 displayName: 'Run Integration Tests' - retryCountOnTaskFailure: 3 inputs: - command: 'test' - projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' + testAssemblyVer2: '**\bin\**\Avalonia.IntegrationTests.Appium.dll' + runSettingsFile: 'tests\Avalonia.IntegrationTests.Appium\record-video.runsettings' + + - task: PublishTestResults@2 + displayName: 'Publish test results' + inputs: + testResultsFormat: 'XUnit' + testResultsFiles: '**/*.trx' + condition: succeededOrFailed() - task: Windows Application Driver@0 inputs: diff --git a/build/Base.props b/build/Base.props index 2d50a7eae0..6b7ae5d677 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,9 +1,6 @@  - - - diff --git a/build/ImageSharp.props b/build/ImageSharp.props index c1eee25ce5..74b9ab7e3c 100644 --- a/build/ImageSharp.props +++ b/build/ImageSharp.props @@ -1,5 +1,5 @@ - + diff --git a/build/SampleApp.props b/build/SampleApp.props index 285f880129..8ecbe902ab 100644 --- a/build/SampleApp.props +++ b/build/SampleApp.props @@ -1,13 +1,22 @@ + WinExe - - + + + + + + + + + + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index bca2ef9eec..628f53bf5c 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -3,6 +3,7 @@ Avalonia 11.2.999 + 11.1.0 Avalonia Team Copyright 2013-$([System.DateTime]::Now.ToString(`yyyy`)) © The AvaloniaUI Project https://avaloniaui.net diff --git a/build/SourceLink.props b/build/SourceLink.props index b0f1f2c2cc..fb014e7e21 100644 --- a/build/SourceLink.props +++ b/build/SourceLink.props @@ -20,6 +20,6 @@ - + diff --git a/build/TrimmingEnable.props b/build/TrimmingEnable.props index 6bb5579b24..1fa14cb022 100644 --- a/build/TrimmingEnable.props +++ b/build/TrimmingEnable.props @@ -11,8 +11,7 @@ true - - + true $(WarningsAsErrors);IL2000;IL2001;IL2002;IL2003;IL2004;IL2005;IL2006;IL2007;IL2008;IL2009;IL2010;IL2011;IL2012;IL2013;IL2014;IL2015;IL2016;IL2017;IL2018;IL2019;IL2020;IL2021;IL2022;IL2023;IL2024;IL2025;IL2026;IL2027;IL2028;IL2029;IL2030;IL2031;IL2032;IL2033;IL2034;IL2035;IL2036;IL2037;IL2038;IL2039;IL2040;IL2041;IL2042;IL2043;IL2044;IL2045;IL2046;IL2047;IL2048;IL2049;IL2050;IL2051;IL2052;IL2053;IL2054;IL2055;IL2056;IL2057;IL2058;IL2059;IL2060;IL2061;IL2062;IL2063;IL2064;IL2065;IL2066;IL2067;IL2068;IL2069;IL2070;IL2071;IL2072;IL2073;IL2074;IL2075;IL2076;IL2077;IL2078;IL2079;IL2080;IL2081;IL2082;IL2083;IL2084;IL2085;IL2086;IL2087;IL2088;IL2089;IL2090;IL2091;IL2092;IL2093;IL2094;IL2095;IL2096;IL2097;IL2098;IL2099;IL2100;IL2101;IL2102;IL2103;IL2104;IL2105;IL2106;IL2107;IL2108;IL2109;IL2110;IL2111;IL2112;IL2113;IL2114;IL2115;IL2116;IL2117;IL2118;IL2119;IL2120;IL2121;IL2122;IL2123;IL2124;IL2125;IL2126;IL2127;IL2128;IL2129;IL2130;IL2131;IL2132;IL2133;IL2134;IL2135;IL2136;IL2137;IL2138;IL2139;IL2140;IL2141;IL2142;IL2143;IL2144;IL2145;IL2146;IL2147;IL2148;IL2149;IL2150;IL2151;IL2152;IL2153;IL2154;IL2155;IL2156;IL2157 diff --git a/global.json b/global.json index 8e84e96f22..b423edba49 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.101", + "version": "8.0.204", "rollForward": "latestFeature" }, "msbuild-sdks": { diff --git a/native/Avalonia.Native/inc/comimpl.h b/native/Avalonia.Native/inc/comimpl.h index 017de01ee9..fd9093bb41 100644 --- a/native/Avalonia.Native/inc/comimpl.h +++ b/native/Avalonia.Native/inc/comimpl.h @@ -9,7 +9,7 @@ #include /** - START_COM_CALL causes AddRef to be called at the beggining of a function. + START_COM_CALL causes AddRef to be called at the beginning of a function. When a function is exited, it causes ReleaseRef to be called. This ensures that the object cannot be destroyed whilst the function is running. For example: Window Show is called, which triggers an event, and user calls Close inside the event diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index a07532412d..9a67ee0161 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -17,7 +17,6 @@ 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391CD090AA776E7E841AC9 /* WindowImpl.h */; }; 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839166350F32661F3ABD70F /* AutoFitContentView.mm */; }; 18391AC16726CBC45856233B /* AvnWindow.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839155B28B20FFB672D29C6 /* AvnWindow.mm */; }; - 18391AC65ADD7DDD33FBE737 /* PopupImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 183910513F396141938832B5 /* PopupImpl.h */; }; 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839171D898F9BFC1373631A /* ResizeScope.h */; }; 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */ = {isa = PBXBuildFile; fileRef = 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */; }; 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839132D0E2454D911F1D1F9 /* AvnView.mm */; }; @@ -35,7 +34,7 @@ 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; }; 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; - 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; + 37C09D8821580FE4006A6758 /* StorageProvider.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* StorageProvider.mm */; }; 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; }; 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; }; 520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; }; @@ -58,13 +57,15 @@ AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; }; BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; + BC7C33822C066DBF00945A48 /* AvnAutomationNode.h in Headers */ = {isa = PBXBuildFile; fileRef = BC7C33812C066DBF00945A48 /* AvnAutomationNode.h */; }; ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; }; ED754D262A97306B0078B4DF /* PlatformRenderTimer.mm in Sources */ = {isa = PBXBuildFile; fileRef = ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */; }; EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */; }; + F10084842BFF1F9E0024303E /* TopLevelImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = F10084832BFF1F9E0024303E /* TopLevelImpl.h */; }; + F10084862BFF1FB40024303E /* TopLevelImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = F10084852BFF1FB40024303E /* TopLevelImpl.mm */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 183910513F396141938832B5 /* PopupImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PopupImpl.h; sourceTree = ""; }; 1839122E037567BDD1D09DEB /* WindowProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowProtocol.h; sourceTree = ""; }; 1839132D0E2454D911F1D1F9 /* AvnView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnView.mm; sourceTree = ""; }; 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IWindowStateChanged.h; sourceTree = ""; }; @@ -94,7 +95,7 @@ 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; 37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; }; - 37C09D8721580FE4006A6758 /* SystemDialogs.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SystemDialogs.mm; sourceTree = ""; }; + 37C09D8721580FE4006A6758 /* StorageProvider.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = StorageProvider.mm; sourceTree = ""; }; 37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = ""; }; 37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; }; 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; @@ -122,9 +123,13 @@ AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platformthreading.mm; sourceTree = ""; }; BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = ""; }; BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = ""; }; + BC7C33812C066DBF00945A48 /* AvnAutomationNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvnAutomationNode.h; sourceTree = ""; }; + BC7C33832C066F1100945A48 /* AvnAccessibility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnAccessibility.h; sourceTree = ""; }; ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformRenderTimer.mm; sourceTree = ""; }; EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformSettings.mm; sourceTree = ""; }; + F10084832BFF1F9E0024303E /* TopLevelImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TopLevelImpl.h; sourceTree = ""; }; + F10084852BFF1FB40024303E /* TopLevelImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TopLevelImpl.mm; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -164,9 +169,13 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + F10084852BFF1FB40024303E /* TopLevelImpl.mm */, + BC7C33832C066F1100945A48 /* AvnAccessibility.h */, + BC7C33812C066DBF00945A48 /* AvnAutomationNode.h */, ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */, 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */, 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */, + F10084832BFF1F9E0024303E /* TopLevelImpl.h */, 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */, 8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */, BC11A5BC2608D58F0017BAD0 /* automation.h */, @@ -193,7 +202,7 @@ 523484CB26EA68AA00EA0C2C /* trayicon.h */, 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */, 37A517B22159597E00FBA241 /* Screens.mm */, - 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, + 37C09D8721580FE4006A6758 /* StorageProvider.mm */, EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */, AB7A61F02147C815003C5833 /* Products */, AB661C1C2148230E00291242 /* Frameworks */, @@ -214,7 +223,6 @@ 1839155B28B20FFB672D29C6 /* AvnWindow.mm */, 18391DB45C7D892E61BF388C /* WindowInterfaces.h */, 18391BB698579F40F1783F31 /* PopupImpl.mm */, - 183910513F396141938832B5 /* PopupImpl.h */, 64B1EBEECBE13D8616D7C934 /* metal.mm */, 64B1E4FA7D9D6E5F47AA8606 /* noarc.mm */, 64B1E26F2B1B9C577BF52F06 /* noarc.h */, @@ -237,10 +245,12 @@ buildActionMask = 2147483647; files = ( 37155CE4233C00EB0034DCE9 /* menu.h in Headers */, + F10084842BFF1F9E0024303E /* TopLevelImpl.h in Headers */, BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */, 183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */, 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */, 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */, + BC7C33822C066DBF00945A48 /* AvnAutomationNode.h in Headers */, 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */, 8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */, 8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */, @@ -249,7 +259,6 @@ 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */, 18391F1E2411C79405A9943A /* WindowProtocol.h in Headers */, 183914E50CF6D2EFC1667F7C /* WindowInterfaces.h in Headers */, - 18391AC65ADD7DDD33FBE737 /* PopupImpl.h in Headers */, 64B1ECA861163C0EFF0E502B /* noarc.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -330,8 +339,9 @@ 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */, 1A465D10246AB61600C5858B /* dnd.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, - 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, + 37C09D8821580FE4006A6758 /* StorageProvider.mm in Sources */, 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */, + F10084862BFF1FB40024303E /* TopLevelImpl.mm in Sources */, 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */, 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */, 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme index c1e5b91e3e..5014c5d439 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> diff --git a/native/Avalonia.Native/src/OSX/AvnAccessibility.h b/native/Avalonia.Native/src/OSX/AvnAccessibility.h new file mode 100644 index 0000000000..6658d8523e --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnAccessibility.h @@ -0,0 +1,13 @@ +#pragma once +#import +#import "avalonia-native.h" + +// Defines the interface between AvnAutomationNode and objects which implement +// NSAccessibility such as AvnAccessibilityElement or AvnWindow. +@protocol AvnAccessibility +@required +- (void) raiseChildrenChanged; +@optional +- (void) raiseFocusChanged; +- (void) raisePropertyChanged:(AvnAutomationProperty)property; +@end diff --git a/native/Avalonia.Native/src/OSX/AvnAutomationNode.h b/native/Avalonia.Native/src/OSX/AvnAutomationNode.h new file mode 100644 index 0000000000..58bea8caf9 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnAutomationNode.h @@ -0,0 +1,18 @@ +#pragma once +#include "avalonia-native.h" +#include "AvnAccessibility.h" + +// Defines a means for managed code to raise accessibility events. +class AvnAutomationNode : public ComSingleObject +{ +public: + FORWARD_IUNKNOWN() + AvnAutomationNode(id owner) { _owner = owner; } + AvnAccessibilityElement* GetOwner() { return _owner; } + virtual void Dispose() override { _owner = nil; } + virtual void ChildrenChanged () override { [_owner raiseChildrenChanged]; } + virtual void PropertyChanged (AvnAutomationProperty property) override { [_owner raisePropertyChanged:property]; } + virtual void FocusChanged () override { [_owner raiseFocusChanged]; } +private: + __strong id _owner; +}; diff --git a/native/Avalonia.Native/src/OSX/AvnView.h b/native/Avalonia.Native/src/OSX/AvnView.h index e058656d3f..75a5e98ed4 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.h +++ b/native/Avalonia.Native/src/OSX/AvnView.h @@ -7,20 +7,20 @@ #import #include "common.h" -#include "WindowImpl.h" +#include "TopLevelImpl.h" #include "KeyTransform.h" @class AvnAccessibilityElement; @protocol IRenderTarget; @interface AvnView : NSView --(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; +-(AvnView* _Nonnull) initWithParent: (TopLevelImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; -(AvnPoint) translateLocalPoint:(AvnPoint)pt; -(void) onClosed; -(AvnPlatformResizeReason) getResizeReason; -(void) setResizeReason:(AvnPlatformResizeReason)reason; --(void) setRenderTarget:(NSObject*)target; -+ (AvnPoint)toAvnPoint:(CGPoint)p; +-(void) setRenderTarget:(NSObject* _Nonnull)target; +-(void) raiseAccessibilityChildrenChanged; @end diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index fc0d695384..8c38b1cf2d 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -7,10 +7,11 @@ #include "AvnView.h" #include "automation.h" #import "WindowInterfaces.h" +#import "WindowImpl.h" @implementation AvnView { - ComPtr _parent; + ComPtr _parent; NSTrackingArea* _area; bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; AvnInputModifiers _modifierState; @@ -18,12 +19,12 @@ AvnPixelSize _lastPixelSize; NSObject* _currentRenderTarget; AvnPlatformResizeReason _resizeReason; - AvnAccessibilityElement* _accessibilityChild; NSRect _cursorRect; NSMutableAttributedString* _text; NSRange _selectedRange; NSRange _markedRange; NSEvent* _lastKeyDownEvent; + NSMutableArray* _accessibilityChildren; } - (void)onClosed @@ -67,7 +68,7 @@ [self updateLayer]; } --(AvnView*) initWithParent: (WindowBaseImpl*) parent +-(AvnView*) initWithParent: (TopLevelImpl*) parent { self = [super init]; [self setWantsLayer:YES]; @@ -155,7 +156,7 @@ auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); + _parent->TopLevelEvents->Resized(FromNSSize(newSize), reason); } } @@ -167,14 +168,14 @@ return; } - _parent->BaseEvents->RunRenderPriorityJobs(); + _parent->TopLevelEvents->RunRenderPriorityJobs(); if (_parent == nullptr) { return; } - _parent->BaseEvents->Paint(); + _parent->TopLevelEvents->Paint(); } - (void)drawRect:(NSRect)dirtyRect @@ -188,16 +189,6 @@ return pt; } -+ (AvnPoint)toAvnPoint:(CGPoint)p -{ - AvnPoint result; - - result.X = p.x; - result.Y = p.y; - - return result; -} - - (void) viewDidChangeBackingProperties { auto fsize = [self convertSizeToBacking: [self frame].size]; @@ -207,7 +198,7 @@ if(_parent != nullptr) { - _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + _parent->TopLevelEvents->ScalingChanged([[self window] backingScaleFactor]); } [super viewDidChangeBackingProperties]; @@ -219,19 +210,24 @@ { return TRUE; } + + id parentWindow = nullptr; - auto parentWindow = _parent->GetWindowProtocol(); + if([[self window] conformsToProtocol:@protocol(AvnWindowProtocol)]){ + parentWindow = (id)[self window]; + } - if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) + if(parentWindow != nullptr && ![parentWindow shouldTryToHandleEvents]) { if(trigerInputWhenDisabled) { - auto window = dynamic_cast(_parent.getRaw()); - - if(window != nullptr) - { - window->WindowEvents->GotInputWhenDisabled(); + WindowImpl* windowImpl = dynamic_cast(_parent.getRaw()); + + if(windowImpl == nullptr){ + return FALSE; } + + windowImpl->WindowEvents->GotInputWhenDisabled(); } return TRUE; @@ -249,9 +245,13 @@ return; } - auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; - auto avnPoint = [AvnView toAvnPoint:localPoint]; - auto point = [self translateLocalPoint:avnPoint]; + NSPoint eventLocation = [event locationInWindow]; + + auto viewLocation = [self convertPoint:NSMakePoint(0, 0) toView:nil]; + + auto localPoint = NSMakePoint(eventLocation.x - viewLocation.x, viewLocation.y - eventLocation.y); + + auto point = ToAvnPoint(localPoint); AvnVector delta = { 0, 0}; if(type == Wheel) @@ -297,11 +297,26 @@ ) ) ) - [self becomeFirstResponder]; + { + WindowBaseImpl* windowBase = dynamic_cast(_parent.getRaw()); + + if(windowBase != nullptr){ + WindowBaseImpl* parent = windowBase->Parent; + + if(parent != nullptr){ + auto parentWindow = parent->Window; + + [parentWindow makeFirstResponder:parent->View]; + } + } else{ + [self becomeFirstResponder]; + } + } + if(_parent != nullptr) { - _parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta); + _parent->TopLevelEvents->RawMouseEvent(type, timestamp, modifiers, point, delta); } [super mouseMoved:event]; @@ -309,7 +324,7 @@ - (BOOL) resignFirstResponder { - _parent->BaseEvents->LostFocus(); + _parent->TopLevelEvents->LostFocus(); return YES; } @@ -436,6 +451,7 @@ - (void)mouseEntered:(NSEvent *)event { + [self mouseEvent:event withType:Move]; [super mouseEntered:event]; } @@ -461,7 +477,7 @@ auto timestamp = static_cast([event timestamp] * 1000); auto modifiers = [self getModifiers:[event modifierFlags]]; - _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key, physicalKey, keySymbolUtf8); + _parent->TopLevelEvents->RawKeyEvent(type, timestamp, modifiers, key, physicalKey, keySymbolUtf8); } - (void)flagsChanged:(NSEvent *)event @@ -521,7 +537,7 @@ } - (bool) handleKeyDown: (NSTimeInterval) timestamp withKey:(AvnKey)key withPhysicalKey:(AvnPhysicalKey)physicalKey withModifiers:(AvnInputModifiers)modifiers withKeySymbol:(NSString*)keySymbol { - return _parent->BaseEvents->RawKeyEvent(KeyDown, timestamp, modifiers, key, physicalKey, [keySymbol UTF8String]); + return _parent->TopLevelEvents->RawKeyEvent(KeyDown, timestamp, modifiers, key, physicalKey, [keySymbol UTF8String]); } - (void)keyDown:(NSEvent *)event @@ -575,7 +591,7 @@ if(keySymbol != nullptr && key != AvnKeyEnter){ auto timestamp = static_cast([event timestamp] * 1000); - _parent->BaseEvents->RawTextInputEvent(timestamp, [keySymbol UTF8String]); + _parent->TopLevelEvents->RawTextInputEvent(timestamp, [keySymbol UTF8String]); } } } @@ -707,7 +723,7 @@ uint64_t timestamp = static_cast([NSDate timeIntervalSinceReferenceDate] * 1000); - _parent->BaseEvents->RawTextInputEvent(timestamp, [text UTF8String]); + _parent->TopLevelEvents->RawTextInputEvent(timestamp, [text UTF8String]); } - (NSUInteger)characterIndexForPoint:(NSPoint)point @@ -727,13 +743,13 @@ - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info { auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; - auto avnPoint = [AvnView toAvnPoint:localPoint]; + auto avnPoint = ToAvnPoint(localPoint); auto point = [self translateLocalPoint:avnPoint]; auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; NSDragOperation nsop = [info draggingSourceOperationMask]; auto effects = ConvertDragDropEffects(nsop); - int reffects = (int)_parent->BaseEvents + int reffects = (int)_parent->TopLevelEvents ->DragEvent(type, point, modifiers, effects, CreateClipboard([info draggingPasteboard], nil), GetAvnDataObjectHandleFromDraggingInfo(info)); @@ -795,35 +811,74 @@ _resizeReason = reason; } -- (AvnAccessibilityElement *) accessibilityChild +- (NSArray *)accessibilityChildren { - if (_accessibilityChild == nil) - { - auto peer = _parent->BaseEvents->GetAutomationPeer(); + if (_accessibilityChildren == nil) + [self recalculateAccessibiltyChildren]; + return _accessibilityChildren; +} - if (peer == nil) - return nil; +- (id _Nullable) accessibilityHitTest:(NSPoint)point +{ + if (![[self window] isKindOfClass:[AvnWindow class]]) + return self; - _accessibilityChild = [AvnAccessibilityElement acquire:peer]; - } + auto window = (AvnWindow*)[self window]; + auto peer = [window automationPeer]; - return _accessibilityChild; -} + if (!peer->IsRootProvider()) + return nil; -- (NSArray *)accessibilityChildren -{ - auto child = [self accessibilityChild]; - return NSAccessibilityUnignoredChildrenForOnlyChild(child); + auto clientPoint = [window convertPointFromScreen:point]; + auto localPoint = [self translateLocalPoint:ToAvnPoint(clientPoint)]; + auto hit = peer->RootProvider_GetPeerFromPoint(localPoint); + return [AvnAccessibilityElement acquire:hit]; } -- (id)accessibilityHitTest:(NSPoint)point +- (void)raiseAccessibilityChildrenChanged { - return [[self accessibilityChild] accessibilityHitTest:point]; + auto changed = _accessibilityChildren ? [NSMutableSet setWithArray:_accessibilityChildren] : [NSMutableSet set]; + + [self recalculateAccessibiltyChildren]; + + if (_accessibilityChildren) + [changed addObjectsFromArray:_accessibilityChildren]; + + NSAccessibilityPostNotificationWithUserInfo( + self, + NSAccessibilityLayoutChangedNotification, + @{ NSAccessibilityUIElementsKey: [changed allObjects]}); } -- (id)accessibilityFocusedUIElement +- (void)recalculateAccessibiltyChildren { - return [[self accessibilityChild] accessibilityFocusedUIElement]; + _accessibilityChildren = [[NSMutableArray alloc] init]; + + if (![[self window] isKindOfClass:[AvnWindow class]]) + { + return; + } + + // The accessibility children of the Window are exposed as children + // of the AvnView. + auto window = (AvnWindow*)[self window]; + auto peer = [window automationPeer]; + auto childPeers = peer->GetChildren(); + auto childCount = childPeers != nullptr ? childPeers->GetCount() : 0; + + if (childCount > 0) + { + for (int i = 0; i < childCount; ++i) + { + IAvnAutomationPeer* child; + + if (childPeers->Get(i, &child) == S_OK) + { + id element = [AvnAccessibilityElement acquire:child]; + [_accessibilityChildren addObject:element]; + } + } + } } - (void) setText:(NSString *)text{ diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index ef50cdab84..9b92dba523 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -24,7 +24,8 @@ #include "WindowImpl.h" #include "AvnView.h" #include "WindowInterfaces.h" -#include "PopupImpl.h" +#include "AvnAutomationNode.h" +#include "AvnString.h" @implementation CLASS_NAME { @@ -35,6 +36,13 @@ bool _isExtended; bool _isTransitioningToFullScreen; AvnMenu* _menu; + IAvnAutomationPeer* _automationPeer; + AvnAutomationNode* _automationNode; +} + +-(AvnView* _Nullable) view +{ + return _parent->View; } -(void) setIsExtended:(bool)value; @@ -201,8 +209,6 @@ [self backingScaleFactor]; } - - - (void)windowWillClose:(NSNotification *_Nonnull)notification { _closed = true; @@ -211,7 +217,7 @@ ComPtr parent = _parent; _parent = NULL; - auto window = dynamic_cast(parent.getRaw()); + auto window = dynamic_cast(parent.getRaw()); if(window != nullptr) { @@ -231,7 +237,7 @@ // // If we don't implement this, then isZoomed always returns true for a non- // resizable window ¯\_(ツ)_/¯ -- (NSRect)windowWillUseStandardFrame:(NSWindow*)window +- (NSRect)windowWillUseStandardFrame:(NSWindow* _Nonnull)window defaultFrame:(NSRect)newFrame { return newFrame; } @@ -397,7 +403,7 @@ return _parent->CanZoom(); } --(void)windowDidResignKey:(NSNotification *)notification +-(void)windowDidResignKey:(NSNotification* _Nonnull)notification { if(_parent) _parent->BaseEvents->Deactivated(); @@ -456,7 +462,7 @@ if (!NSPointInRect(viewPoint, view.bounds)) { - auto avnPoint = [AvnView toAvnPoint:windowPoint]; + auto avnPoint = ToAvnPoint(windowPoint); auto point = [self translateLocalPoint:avnPoint]; AvnVector delta = { 0, 0 }; @@ -492,5 +498,41 @@ _parent = nullptr; } +- (id _Nullable) accessibilityFocusedUIElement +{ + if (![self automationPeer]->IsRootProvider()) + return nil; + auto focusedPeer = [self automationPeer]->RootProvider_GetFocus(); + return [AvnAccessibilityElement acquire:focusedPeer]; +} + +- (NSString * _Nullable) accessibilityIdentifier +{ + return GetNSStringAndRelease([self automationPeer]->GetAutomationId()); +} + +- (IAvnAutomationPeer* _Nonnull) automationPeer +{ + if (_automationPeer == nullptr) + { + _automationPeer = _parent->BaseEvents->GetAutomationPeer(); + _automationNode = new AvnAutomationNode(self); + _automationPeer->SetNode(_automationNode); + } + + return _automationPeer; +} + +- (void)raiseChildrenChanged +{ + [_parent->View raiseAccessibilityChildrenChanged]; +} + +- (void)raiseFocusChanged +{ + id focused = [self accessibilityFocusedUIElement]; + NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification); +} + @end diff --git a/native/Avalonia.Native/src/OSX/INSWindowHolder.h b/native/Avalonia.Native/src/OSX/INSWindowHolder.h index 3c5010966b..7c07d6bb40 100644 --- a/native/Avalonia.Native/src/OSX/INSWindowHolder.h +++ b/native/Avalonia.Native/src/OSX/INSWindowHolder.h @@ -11,6 +11,10 @@ struct INSWindowHolder { virtual NSWindow* _Nonnull GetNSWindow () = 0; +}; + +struct INSViewHolder +{ virtual AvnView* _Nonnull GetNSView () = 0; }; diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.h b/native/Avalonia.Native/src/OSX/PopupImpl.h deleted file mode 100644 index 451019a6a4..0000000000 --- a/native/Avalonia.Native/src/OSX/PopupImpl.h +++ /dev/null @@ -1,9 +0,0 @@ -// -// Created by Dan Walmsley on 06/05/2022. -// Copyright (c) 2022 Avalonia. All rights reserved. -// - -#ifndef AVALONIA_NATIVE_OSX_POPUPIMPL_H -#define AVALONIA_NATIVE_OSX_POPUPIMPL_H - -#endif //AVALONIA_NATIVE_OSX_POPUPIMPL_H diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.mm b/native/Avalonia.Native/src/OSX/PopupImpl.mm index 39aa568134..40fe8ce88b 100644 --- a/native/Avalonia.Native/src/OSX/PopupImpl.mm +++ b/native/Avalonia.Native/src/OSX/PopupImpl.mm @@ -12,7 +12,6 @@ #import "WindowBaseImpl.h" #import "WindowProtocol.h" #import -#include "PopupImpl.h" class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup { @@ -23,7 +22,7 @@ private: END_INTERFACE_MAP() virtual ~PopupImpl(){} ComPtr WindowEvents; - PopupImpl(IAvnWindowEvents* events) : WindowBaseImpl(events) + PopupImpl(IAvnWindowEvents* events) : TopLevelImpl(events), WindowBaseImpl(events) { WindowEvents = events; [Window setLevel:NSPopUpMenuWindowLevel]; @@ -35,13 +34,12 @@ protected: } public: - virtual bool ShouldTakeFocusOnShow() override - { - return false; - } - virtual HRESULT Show(bool activate, bool isDialog) override { + auto windowProtocol = GetWindowProtocol(); + + [windowProtocol setEnabled:true]; + return WindowBaseImpl::Show(activate, true); } }; diff --git a/native/Avalonia.Native/src/OSX/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index 85f4b7c50a..2279dae7c8 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -1,36 +1,62 @@ #include "common.h" +#include "AvnString.h" class Screens : public ComSingleObject { - public: - FORWARD_IUNKNOWN() - +private: + ComPtr _events; public: - virtual HRESULT GetScreenCount (int* ret) override + FORWARD_IUNKNOWN() + + Screens(IAvnScreenEvents* events) { + _events = events; + CGDisplayRegisterReconfigurationCallback(CGDisplayReconfigurationCallBack, this); + } + + virtual HRESULT GetScreenIds ( + unsigned int* ptrFirstResult, + int* screenCound) override { START_COM_CALL; @autoreleasepool { - *ret = (int)[NSScreen screens].count; - + auto screens = [NSScreen screens]; + *screenCound = (int)screens.count; + + if (ptrFirstResult == nil) + return S_OK; + + for (int i = 0; i < screens.count; i++) { + ptrFirstResult[i] = [[screens objectAtIndex:i] av_displayId]; + } + return S_OK; } } - - virtual HRESULT GetScreen (int index, AvnScreen* ret) override - { + + virtual HRESULT GetScreen ( + CGDirectDisplayID displayId, + void** localizedName, + AvnScreen* ret + ) override { START_COM_CALL; @autoreleasepool { - if(index < 0 || index >= [NSScreen screens].count) - { - return E_INVALIDARG; + NSScreen* screen; + for (NSScreen *s in NSScreen.screens) { + if (s.av_displayId == displayId) + { + screen = s; + break; + } } - auto screen = [[NSScreen screens] objectAtIndex:index]; - + if (screen == nil) { + return E_INVALIDARG; + } + ret->Bounds.Height = [screen frame].size.height; ret->Bounds.Width = [screen frame].size.width; ret->Bounds.X = [screen frame].origin.x; @@ -43,14 +69,45 @@ public: ret->Scaling = 1; - ret->IsPrimary = index == 0; - + ret->IsPrimary = CGDisplayIsMain(displayId); + + // Compute natural orientation: + auto naturalScreenSize = CGDisplayScreenSize(displayId); + auto isNaturalLandscape = naturalScreenSize.width > naturalScreenSize.height; + // Normalize rotation: + auto rotation = (int)CGDisplayRotation(displayId) % 360; + if (rotation < 0) rotation = 360 - rotation; + // Get current orientation relative to the natural + if (rotation >= 0 && rotation < 90) { + ret->Orientation = isNaturalLandscape ? AvnScreenOrientation::Landscape : AvnScreenOrientation::Portrait; + } else if (rotation >= 90 && rotation < 180) { + ret->Orientation = isNaturalLandscape ? AvnScreenOrientation::Portrait : AvnScreenOrientation::Landscape; + } else if (rotation >= 180 && rotation < 270) { + ret->Orientation = isNaturalLandscape ? AvnScreenOrientation::LandscapeFlipped : AvnScreenOrientation::PortraitFlipped; + } else { + ret->Orientation = isNaturalLandscape ? AvnScreenOrientation::PortraitFlipped : AvnScreenOrientation::LandscapeFlipped; + } + + if (@available(macOS 10.15, *)) { + *localizedName = CreateAvnString([screen localizedName]); + } + return S_OK; } } + +private: + static void CGDisplayReconfigurationCallBack(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void *screens) + { + auto object = (Screens *)screens; + auto events = object->_events; + if (events != nil) { + events->OnChanged(); + } + } }; -extern IAvnScreens* CreateScreens() +extern IAvnScreens* CreateScreens(IAvnScreenEvents* events) { - return new Screens(); + return new Screens(events); } diff --git a/native/Avalonia.Native/src/OSX/SystemDialogs.mm b/native/Avalonia.Native/src/OSX/StorageProvider.mm similarity index 84% rename from native/Avalonia.Native/src/OSX/SystemDialogs.mm rename to native/Avalonia.Native/src/OSX/StorageProvider.mm index c09464af4f..0fd77c6789 100644 --- a/native/Avalonia.Native/src/OSX/SystemDialogs.mm +++ b/native/Avalonia.Native/src/OSX/StorageProvider.mm @@ -64,12 +64,91 @@ const int kFileTypePopupTag = 10975; @end -class SystemDialogs : public ComSingleObject +class StorageProvider : public ComSingleObject { ExtensionDropdownHandler* __strong _extension_dropdown_handler; public: FORWARD_IUNKNOWN() + + virtual HRESULT SaveBookmarkToBytes ( + IAvnString* fileUriStr, + void** err, + IAvnString** ppv + ) override + { + @autoreleasepool + { + if(ppv == nullptr) + return E_POINTER; + + NSError* error; + auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)]; + auto bookmarkData = [fileUri bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; + if (bookmarkData) + { + *ppv = CreateByteArray((void*)bookmarkData.bytes, (int)bookmarkData.length); + } + if (error != nil) + { + *err = CreateAvnString([error localizedDescription]); + } + return S_OK; + } + } + + virtual HRESULT ReadBookmarkFromBytes ( + void* ptr, + int len, + IAvnString** ppv + ) override { + @autoreleasepool + { + if(ppv == nullptr) + return E_POINTER; + + auto bookmarkData = [[NSData alloc] initWithBytes:ptr length:len]; + auto fileUri = [NSURL URLByResolvingBookmarkData: bookmarkData + options:NSURLBookmarkResolutionWithSecurityScope|NSURLBookmarkResolutionWithoutUI + relativeToURL:nil + bookmarkDataIsStale:nil + error:nil]; + + if (fileUri) + { + *ppv = CreateAvnString([fileUri absoluteString]); + } + return S_OK; + } + } + + virtual void ReleaseBookmark ( + IAvnString* fileUriStr + ) override { + // no-op + } + + virtual bool OpenSecurityScope ( + IAvnString* fileUriStr + ) override { + @autoreleasepool + { + auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)]; + auto success = [fileUri startAccessingSecurityScopedResource]; + return success; + } + } + + virtual void CloseSecurityScope ( + IAvnString* fileUriStr + ) override { + @autoreleasepool + { + auto fileUri = [NSURL URLWithString: GetNSStringAndRelease(fileUriStr)]; + [fileUri stopAccessingSecurityScopedResource]; + } + } + virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle, IAvnSystemDialogEvents* events, bool allowMultiple, @@ -94,7 +173,8 @@ public: if(initialDirectory != nullptr) { auto directoryString = [NSString stringWithUTF8String:initialDirectory]; - panel.directoryURL = [NSURL fileURLWithPath:directoryString]; + panel.directoryURL = [NSURL fileURLWithPath:directoryString + isDirectory:true]; } auto handler = ^(NSModalResponse result) { @@ -104,19 +184,9 @@ public: if(urls.count > 0) { - void* strings[urls.count]; - - for(int i = 0; i < urls.count; i++) - { - auto url = [urls objectAtIndex:i]; - - auto string = [url path]; - - strings[i] = (void*)[string UTF8String]; - } - - events->OnCompleted((int)urls.count, &strings[0]); - + auto uriStrings = CreateAvnStringArray(urls); + events->OnCompleted(uriStrings); + [panel orderOut:panel]; if(parentWindowHandle != nullptr) @@ -129,7 +199,7 @@ public: } } - events->OnCompleted(0, nullptr); + events->OnCompleted(nullptr); }; @@ -169,7 +239,8 @@ public: if(initialDirectory != nullptr) { auto directoryString = [NSString stringWithUTF8String:initialDirectory]; - panel.directoryURL = [NSURL fileURLWithPath:directoryString]; + panel.directoryURL = [NSURL fileURLWithPath:directoryString + isDirectory:true]; } if(initialFile != nullptr) @@ -186,19 +257,9 @@ public: if(urls.count > 0) { - void* strings[urls.count]; - - for(int i = 0; i < urls.count; i++) - { - auto url = [urls objectAtIndex:i]; - - auto string = [url path]; - - strings[i] = (void*)[string UTF8String]; - } - - events->OnCompleted((int)urls.count, &strings[0]); - + auto uriStrings = CreateAvnStringArray(urls); + events->OnCompleted(uriStrings); + [panel orderOut:panel]; if(parentWindowHandle != nullptr) @@ -211,7 +272,7 @@ public: } } - events->OnCompleted(0, nullptr); + events->OnCompleted(nullptr); }; @@ -248,7 +309,8 @@ public: if(initialDirectory != nullptr) { auto directoryString = [NSString stringWithUTF8String:initialDirectory]; - panel.directoryURL = [NSURL fileURLWithPath:directoryString]; + panel.directoryURL = [NSURL fileURLWithPath:directoryString + isDirectory:true]; } if(initialFile != nullptr) @@ -261,15 +323,11 @@ public: auto handler = ^(NSModalResponse result) { if(result == NSFileHandlingPanelOKButton) { - void* strings[1]; - auto url = [panel URL]; - - auto string = [url path]; - strings[0] = (void*)[string UTF8String]; - - events->OnCompleted(1, &strings[0]); - + auto urls = [NSArray arrayWithObject:url]; + auto uriStrings = CreateAvnStringArray(urls); + events->OnCompleted(uriStrings); + [panel orderOut:panel]; if(parentWindowHandle != nullptr) @@ -281,7 +339,7 @@ public: return; } - events->OnCompleted(0, nullptr); + events->OnCompleted(nullptr); }; @@ -516,7 +574,7 @@ private: }; }; -extern IAvnSystemDialogs* CreateSystemDialogs() +extern IAvnStorageProvider* CreateStorageProvider() { - return new SystemDialogs(); + return new StorageProvider(); } diff --git a/native/Avalonia.Native/src/OSX/TopLevelImpl.h b/native/Avalonia.Native/src/OSX/TopLevelImpl.h new file mode 100644 index 0000000000..dd494ab761 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/TopLevelImpl.h @@ -0,0 +1,77 @@ +// +// TopLevelImpl.h +// Avalonia.Native.OSX +// +// Created by Benedikt Stebner on 16.05.24. +// Copyright © 2024 Avalonia. All rights reserved. +// + +#ifndef TopLevelImpl_h +#define TopLevelImpl_h + +#include "rendertarget.h" +#include "INSWindowHolder.h" +#include "AvnTextInputMethod.h" +#include "AutoFitContentView.h" +#include + +class TopLevelImpl : public virtual ComObject, + public virtual IAvnTopLevel, + public INSViewHolder{ + +public: + FORWARD_IUNKNOWN() + BEGIN_INTERFACE_MAP() + INTERFACE_MAP_ENTRY(IAvnTopLevel, IID_IAvnTopLevel) + END_INTERFACE_MAP() + + virtual ~TopLevelImpl(); + + TopLevelImpl(IAvnTopLevelEvents* events); + + virtual AvnView *GetNSView() override; + + virtual HRESULT SetCursor(IAvnCursor* cursor) override; + + virtual HRESULT GetScaling(double*ret) override; + + virtual HRESULT GetClientSize(AvnSize *ret) override; + + virtual HRESULT GetInputMethod(IAvnTextInputMethod **ppv) override; + + virtual HRESULT ObtainNSViewHandle(void** retOut) override; + + virtual HRESULT ObtainNSViewHandleRetained(void** retOut) override; + + virtual HRESULT CreateSoftwareRenderTarget(IAvnSoftwareRenderTarget** ret) override; + + virtual HRESULT CreateMetalRenderTarget(IAvnMetalDevice* device, IAvnMetalRenderTarget** ret) override; + + virtual HRESULT CreateGlRenderTarget(IAvnGlContext* context, IAvnGlSurfaceRenderTarget** ret) override; + + virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost **retOut) override; + + virtual HRESULT Invalidate() override; + + virtual HRESULT PointToClient(AvnPoint point, AvnPoint *ret) override; + + virtual HRESULT PointToScreen(AvnPoint point, AvnPoint *ret) override; + + virtual HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode) override; + + virtual HRESULT GetCurrentDisplayId (CGDirectDisplayID* ret) override; +protected: + NSCursor *cursor; + virtual void UpdateAppearance(); + +public: + NSObject *currentRenderTarget; + ComPtr InputMethod; + ComPtr TopLevelEvents; + AvnView *View; + + void UpdateCursor(); + virtual void SetClientSize(NSSize size); +}; + +#endif /* TopLevelImpl_h */ diff --git a/native/Avalonia.Native/src/OSX/TopLevelImpl.mm b/native/Avalonia.Native/src/OSX/TopLevelImpl.mm new file mode 100644 index 0000000000..6200f096d3 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/TopLevelImpl.mm @@ -0,0 +1,285 @@ +#import +#import +#include "automation.h" +#include "cursor.h" +#include "AutoFitContentView.h" +#include "TopLevelImpl.h" +#include "AvnTextInputMethod.h" +#include "AvnView.h" + +TopLevelImpl::~TopLevelImpl() { + View = nullptr; +} + +TopLevelImpl::TopLevelImpl(IAvnTopLevelEvents *events) { + TopLevelEvents = events; + + View = [[AvnView alloc] initWithParent:this]; + InputMethod = new AvnTextInputMethod(View); +} + +HRESULT TopLevelImpl::GetScaling(double *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) + return E_POINTER; + + if ([View window] == nullptr) { + *ret = 1; + return S_OK; + } + + *ret = [[View window] backingScaleFactor]; + + return S_OK; + } +} + +HRESULT TopLevelImpl::GetClientSize(AvnSize *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) + return E_POINTER; + + NSRect frame = [View frame]; + + ret->Width = frame.size.width; + ret->Height = frame.size.height; + + return S_OK; + } +} + +HRESULT TopLevelImpl::GetInputMethod(IAvnTextInputMethod **retOut) { + START_COM_CALL; + + *retOut = InputMethod; + + return S_OK; +} + +HRESULT TopLevelImpl::ObtainNSViewHandle(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge void *) View; + + return S_OK; +} + +HRESULT TopLevelImpl::ObtainNSViewHandleRetained(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge_retained void *) View; + + return S_OK; +} + +HRESULT TopLevelImpl::SetCursor(IAvnCursor *cursor) { + START_COM_CALL; + + @autoreleasepool { + Cursor *avnCursor = dynamic_cast(cursor); + this->cursor = avnCursor->GetNative(); + UpdateCursor(); + + if (avnCursor->IsHidden()) { + [NSCursor hide]; + } else { + [NSCursor unhide]; + } + + return S_OK; + } +} + +void TopLevelImpl::UpdateCursor() { + if (cursor != nil) { + [cursor set]; + } +} + +HRESULT TopLevelImpl::CreateSoftwareRenderTarget(IAvnSoftwareRenderTarget **ppv) { + START_COM_CALL; + + if(![NSThread isMainThread]) + return COR_E_INVALIDOPERATION; + + if (View == NULL) + return E_FAIL; + + auto target = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: nil]; + *ppv = [target createSoftwareRenderTarget]; + [View setRenderTarget: target]; + return S_OK; +} + +HRESULT TopLevelImpl::CreateGlRenderTarget(IAvnGlContext* glContext, IAvnGlSurfaceRenderTarget **ppv) { + START_COM_CALL; + + if(![NSThread isMainThread]) + return COR_E_INVALIDOPERATION; + + if (View == NULL) + return E_FAIL; + + auto target = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: glContext]; + *ppv = [target createSurfaceRenderTarget]; + [View setRenderTarget: target]; + return S_OK; +} + +HRESULT TopLevelImpl::CreateMetalRenderTarget(IAvnMetalDevice* device, IAvnMetalRenderTarget **ppv) { + START_COM_CALL; + + if(![NSThread isMainThread]) + return COR_E_INVALIDOPERATION; + + if (View == NULL) + return E_FAIL; + + auto target = [[MetalRenderTarget alloc] initWithDevice: device]; + [View setRenderTarget: target]; + [target getRenderTarget: ppv]; + return S_OK; +} + +HRESULT TopLevelImpl::CreateNativeControlHost(IAvnNativeControlHost **retOut) { + START_COM_CALL; + + if (View == NULL) + return E_FAIL; + *retOut = ::CreateNativeControlHost(View); + return S_OK; +} + +AvnView *TopLevelImpl::GetNSView() { + return View; +} + +HRESULT TopLevelImpl::Invalidate() { + START_COM_CALL; + + @autoreleasepool { + [View setNeedsDisplayInRect:[View frame]]; + + return S_OK; + } +} + +HRESULT TopLevelImpl::PointToClient(AvnPoint point, AvnPoint *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + auto window = [View window]; + + if(window == nullptr){ + ret = &point; + + return S_OK; + } + + auto frame = [View frame]; + + auto viewRect = [View convertRect:frame toView:nil]; + + auto viewScreenRect = [window convertRectToScreen:viewRect]; + + auto primaryDisplayHeight = NSMaxY([[[NSScreen screens] firstObject] frame]); + + //Window coord are bottom to top so we need to adjust by primaryScreenHeight + auto viewScreenLocation = NSMakePoint(viewScreenRect.origin.x, primaryDisplayHeight - viewScreenRect.origin.y - frame.size.height); + + //Substract client point from screen position of the view + auto localPoint = NSMakePoint(point.X - viewScreenLocation.x, point.Y - viewScreenLocation.y); + + point = ToAvnPoint(localPoint); + + *ret = point; + + return S_OK; + } +} + +HRESULT TopLevelImpl::PointToScreen(AvnPoint point, AvnPoint *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + auto window = [View window]; + + if(window == nullptr){ + ret = &point; + + return S_OK; + } + + auto frame = [View frame]; + + //Get rect inside current window + auto viewRect = [View convertRect:frame toView:nil]; + + //Get screen rect of the view + auto viewScreenRect = [window convertRectToScreen:viewRect]; + + auto primaryDisplayHeight = NSMaxY([[[NSScreen screens] firstObject] frame]); + + //Window coord are bottom to top so we need to adjust by primaryScreenHeight + auto viewScreenLocation = NSMakePoint(viewScreenRect.origin.x, primaryDisplayHeight - viewScreenRect.origin.y - frame.size.height); + + //Add client point to screen position of the view + auto screenPoint = ToAvnPoint(NSMakePoint(viewScreenLocation.x + point.X, viewScreenLocation.y + point.Y)); + + *ret = screenPoint; + + return S_OK; + } +} + +HRESULT TopLevelImpl::SetTransparencyMode(AvnWindowTransparencyMode mode) { + START_COM_CALL; + + return S_OK; +} + +HRESULT TopLevelImpl::GetCurrentDisplayId (CGDirectDisplayID* ret) { + START_COM_CALL; + + auto window = [View window]; + *ret = [window.screen av_displayId]; + + return S_OK; +} + +void TopLevelImpl::UpdateAppearance() { + +} + +void TopLevelImpl::SetClientSize(NSSize size){ + [View setFrameSize:size]; +} + +extern IAvnTopLevel* CreateAvnTopLevel(IAvnTopLevelEvents* events) +{ + @autoreleasepool + { + IAvnTopLevel* ptr = (IAvnTopLevel*)new TopLevelImpl(events); + return ptr; + } +} diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 83c7aed5d3..ab0a5fb6a1 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -9,19 +9,21 @@ #include "rendertarget.h" #include "INSWindowHolder.h" #include "AvnTextInputMethod.h" +#include "TopLevelImpl.h" +#include -@class AutoFitContentView; @class AvnMenu; @protocol AvnWindowProtocol; -class WindowBaseImpl : public virtual ComObject, +class WindowBaseImpl : public virtual TopLevelImpl, public virtual IAvnWindowBase, public INSWindowHolder { public: FORWARD_IUNKNOWN() -BEGIN_INTERFACE_MAP() + BEGIN_INTERFACE_MAP() + INHERIT_INTERFACE_MAP(TopLevelImpl) INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase) END_INTERFACE_MAP() @@ -33,14 +35,8 @@ BEGIN_INTERFACE_MAP() virtual HRESULT ObtainNSWindowHandleRetained(void **ret) override; - virtual HRESULT ObtainNSViewHandle(void **ret) override; - - virtual HRESULT ObtainNSViewHandleRetained(void **ret) override; - virtual NSWindow *GetNSWindow() override; - virtual AvnView *GetNSView() override; - virtual HRESULT Show(bool activate, bool isDialog) override; virtual bool IsShown (); @@ -55,18 +51,12 @@ BEGIN_INTERFACE_MAP() virtual HRESULT Close() override; - virtual HRESULT GetClientSize(AvnSize *ret) override; - virtual HRESULT GetFrameSize(AvnSize *ret) override; - virtual HRESULT GetScaling(double *ret) override; - virtual HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize) override; virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override; - virtual HRESULT Invalidate(__attribute__((unused)) AvnRect rect) override; - virtual HRESULT SetMainMenu(IAvnMenu *menu) override; virtual HRESULT BeginMoveDrag() override; @@ -77,49 +67,33 @@ BEGIN_INTERFACE_MAP() virtual HRESULT SetPosition(AvnPoint point) override; - virtual HRESULT PointToClient(AvnPoint point, AvnPoint *ret) override; - - virtual HRESULT PointToScreen(AvnPoint point, AvnPoint *ret) override; - - virtual HRESULT SetCursor(IAvnCursor *cursor) override; - - virtual void UpdateCursor(); - - virtual HRESULT CreateSoftwareRenderTarget(IAvnSoftwareRenderTarget **ppv) override; - - virtual HRESULT CreateGlRenderTarget(IAvnGlContext* glContext, IAvnGlSurfaceRenderTarget **ppv) override; - - virtual HRESULT CreateMetalRenderTarget(IAvnMetalDevice* device, IAvnMetalRenderTarget **ppv) override; - - virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost **retOut) override; - - virtual HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode) override; - virtual HRESULT SetFrameThemeVariant(AvnPlatformThemeVariant variant) override; virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard *clipboard, IAvnDndResultCallback *cb, void *sourceHandle) override; + virtual HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode) override; + virtual bool IsModal(); id GetWindowProtocol (); virtual void BringToFront (); - - virtual HRESULT GetInputMethod(IAvnTextInputMethod **retOut) override; virtual bool CanZoom() { return false; } + virtual HRESULT SetParent(IAvnWindowBase* parent) override; + protected: virtual NSWindowStyleMask CalculateStyleMask() = 0; - virtual void UpdateStyle(); + virtual void UpdateAppearance() override; + virtual void SetClientSize(NSSize size) override; private: void CreateNSWindow (bool isDialog); void CleanNSWindow (); - NSCursor *cursor; bool hasPosition; NSSize lastSize; NSSize lastMinSize; @@ -128,16 +102,16 @@ private: bool _inResize; protected: - AvnPoint lastPositionSet; AutoFitContentView *StandardContainer; + AvnPoint lastPositionSet; bool _shown; + std::list _children; + bool _isModal; public: - NSObject *currentRenderTarget; + WindowBaseImpl* Parent; NSWindow * Window; ComPtr BaseEvents; - ComPtr InputMethod; - AvnView *View; }; #endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 3f54680ae7..afae6d6b5b 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -15,22 +15,22 @@ #import "WindowProtocol.h" #import "WindowInterfaces.h" #include "WindowBaseImpl.h" +#include "WindowImpl.h" #include "AvnTextInputMethod.h" #include "AvnView.h" +@class AutoFitContentView; WindowBaseImpl::~WindowBaseImpl() { View = nullptr; Window = nullptr; } -WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, bool usePanel) { +WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, bool usePanel) : TopLevelImpl(events) { + _children = std::list(); _shown = false; _inResize = false; BaseEvents = events; - View = [[AvnView alloc] initWithParent:this]; - InputMethod = new AvnTextInputMethod(View); - StandardContainer = [[AutoFitContentView new] initWithContent:View]; lastPositionSet = { 0, 0 }; hasPosition = false; @@ -41,6 +41,8 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, bool usePanel) { CreateNSWindow(usePanel); + StandardContainer = [[AutoFitContentView new] initWithContent:View]; + [Window setContentView:StandardContainer]; [Window setBackingType:NSBackingStoreBuffered]; [Window setContentMinSize:lastMinSize]; @@ -48,38 +50,10 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, bool usePanel) { [Window setOpaque:false]; } -HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) { - START_COM_CALL; - - if (ret == nullptr) { - return E_POINTER; - } - - *ret = (__bridge void *) View; - - return S_OK; -} - -HRESULT WindowBaseImpl::ObtainNSViewHandleRetained(void **ret) { - START_COM_CALL; - - if (ret == nullptr) { - return E_POINTER; - } - - *ret = (__bridge_retained void *) View; - - return S_OK; -} - NSWindow *WindowBaseImpl::GetNSWindow() { return Window; } -AvnView *WindowBaseImpl::GetNSView() { - return View; -} - HRESULT WindowBaseImpl::ObtainNSWindowHandleRetained(void **ret) { START_COM_CALL; @@ -117,7 +91,7 @@ HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { auto collectionBehavior = [Window collectionBehavior]; [Window setCollectionBehavior:collectionBehavior & ~NSWindowCollectionBehaviorFullScreenPrimary]; - UpdateStyle(); + UpdateAppearance(); [Window invalidateShadow]; @@ -219,20 +193,6 @@ HRESULT WindowBaseImpl::Close() { } } -HRESULT WindowBaseImpl::GetClientSize(AvnSize *ret) { - START_COM_CALL; - - @autoreleasepool { - if (ret == nullptr) - return E_POINTER; - - ret->Width = lastSize.width; - ret->Height = lastSize.height; - - return S_OK; - } -} - HRESULT WindowBaseImpl::GetFrameSize(AvnSize *ret) { START_COM_CALL; @@ -250,23 +210,6 @@ HRESULT WindowBaseImpl::GetFrameSize(AvnSize *ret) { } } -HRESULT WindowBaseImpl::GetScaling(double *ret) { - START_COM_CALL; - - @autoreleasepool { - if (ret == nullptr) - return E_POINTER; - - if (Window == nullptr) { - *ret = 1; - return S_OK; - } - - *ret = [Window backingScaleFactor]; - return S_OK; - } -} - HRESULT WindowBaseImpl::SetMinMaxSize(AvnSize minSize, AvnSize maxSize) { START_COM_CALL; @@ -330,7 +273,7 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso lastSize = NSSize{x, y}; - [Window setContentSize:lastSize]; + SetClientSize(lastSize); [Window invalidateShadow]; } } @@ -342,16 +285,6 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso } } -HRESULT WindowBaseImpl::Invalidate(__attribute__((unused)) AvnRect rect) { - START_COM_CALL; - - @autoreleasepool { - [View setNeedsDisplayInRect:[View frame]]; - - return S_OK; - } -} - HRESULT WindowBaseImpl::SetMainMenu(IAvnMenu *menu) { START_COM_CALL; @@ -431,119 +364,6 @@ HRESULT WindowBaseImpl::SetPosition(AvnPoint point) { } } -HRESULT WindowBaseImpl::PointToClient(AvnPoint point, AvnPoint *ret) { - START_COM_CALL; - - @autoreleasepool { - if (ret == nullptr) { - return E_POINTER; - } - - point = ConvertPointY(point); - NSRect convertRect = [Window convertRectFromScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; - auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); - - *ret = [View translateLocalPoint:ToAvnPoint(viewPoint)]; - - return S_OK; - } -} - -HRESULT WindowBaseImpl::PointToScreen(AvnPoint point, AvnPoint *ret) { - START_COM_CALL; - - @autoreleasepool { - if (ret == nullptr) { - return E_POINTER; - } - - auto cocoaViewPoint = ToNSPoint([View translateLocalPoint:point]); - NSRect convertRect = [Window convertRectToScreen:NSMakeRect(cocoaViewPoint.x, cocoaViewPoint.y, 0.0, 0.0)]; - auto cocoaScreenPoint = NSPointFromCGPoint(NSMakePoint(convertRect.origin.x, convertRect.origin.y)); - *ret = ConvertPointY(ToAvnPoint(cocoaScreenPoint)); - - return S_OK; - } -} - -HRESULT WindowBaseImpl::SetCursor(IAvnCursor *cursor) { - START_COM_CALL; - - @autoreleasepool { - Cursor *avnCursor = dynamic_cast(cursor); - this->cursor = avnCursor->GetNative(); - UpdateCursor(); - - if (avnCursor->IsHidden()) { - [NSCursor hide]; - } else { - [NSCursor unhide]; - } - - return S_OK; - } -} - -void WindowBaseImpl::UpdateCursor() { - if (cursor != nil) { - [cursor set]; - } -} - -HRESULT WindowBaseImpl::CreateSoftwareRenderTarget(IAvnSoftwareRenderTarget **ppv) { - START_COM_CALL; - - if(![NSThread isMainThread]) - return COR_E_INVALIDOPERATION; - - if (View == NULL) - return E_FAIL; - - auto target = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: nil]; - *ppv = [target createSoftwareRenderTarget]; - [View setRenderTarget: target]; - return S_OK; -} - -HRESULT WindowBaseImpl::CreateGlRenderTarget(IAvnGlContext* glContext, IAvnGlSurfaceRenderTarget **ppv) { - START_COM_CALL; - - if(![NSThread isMainThread]) - return COR_E_INVALIDOPERATION; - - if (View == NULL) - return E_FAIL; - - auto target = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: glContext]; - *ppv = [target createSurfaceRenderTarget]; - [View setRenderTarget: target]; - return S_OK; -} - -HRESULT WindowBaseImpl::CreateMetalRenderTarget(IAvnMetalDevice* device, IAvnMetalRenderTarget **ppv) { - START_COM_CALL; - - if(![NSThread isMainThread]) - return COR_E_INVALIDOPERATION; - - if (View == NULL) - return E_FAIL; - - auto target = [[MetalRenderTarget alloc] initWithDevice: device]; - [View setRenderTarget: target]; - [target getRenderTarget: ppv]; - return S_OK; -} - -HRESULT WindowBaseImpl::CreateNativeControlHost(IAvnNativeControlHost **retOut) { - START_COM_CALL; - - if (View == NULL) - return E_FAIL; - *retOut = ::CreateNativeControlHost(View); - return S_OK; -} - HRESULT WindowBaseImpl::SetTransparencyMode(AvnWindowTransparencyMode mode) { START_COM_CALL; @@ -619,10 +439,14 @@ bool WindowBaseImpl::IsModal() { return false; } -void WindowBaseImpl::UpdateStyle() { +void WindowBaseImpl::UpdateAppearance() { [Window setStyleMask:CalculateStyleMask()]; } +void WindowBaseImpl::SetClientSize(NSSize size){ + [Window setContentSize:lastSize]; +} + void WindowBaseImpl::CleanNSWindow() { if(Window != nullptr) { [GetWindowProtocol() disconnectParent]; @@ -654,19 +478,35 @@ void WindowBaseImpl::BringToFront() // do nothing. } -HRESULT WindowBaseImpl::GetInputMethod(IAvnTextInputMethod **retOut) { +HRESULT WindowBaseImpl::SetParent(IAvnWindowBase *parent) { START_COM_CALL; - *retOut = InputMethod; + @autoreleasepool { + if(Parent != nullptr) + { + Parent->_children.remove(this); + } + + auto cparent = dynamic_cast(parent); + + Parent = cparent; - return S_OK; -} + _isModal = Parent != nullptr; + + if(Parent != nullptr && Window != nullptr){ + // If one tries to show a child window with a minimized parent window, then the parent window will be + // restored but macOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive + // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. + if (cparent->WindowState() == Minimized) + cparent->SetWindowState(Normal); + + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; + + cparent->_children.push_back(this); + + UpdateAppearance(); + } -extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events) -{ - @autoreleasepool - { - IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events); - return ptr; + return S_OK; } } diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 047a0d2c84..b931e933db 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -8,26 +8,9 @@ #import "WindowBaseImpl.h" #include "IWindowStateChanged.h" -#include - class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged { -private: - bool _isEnabled; - bool _canResize; - bool _fullScreenActive; - SystemDecorations _decorations; - AvnWindowState _lastWindowState; - AvnWindowState _actualWindowState; - bool _inSetWindowState; - NSRect _preZoomSize; - bool _transitioningWindowState; - bool _isClientAreaExtended; - bool _isModal; - WindowImpl* _parent; - std::list _children; - AvnExtendClientAreaChromeHints _extendClientHints; - +public: FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() INHERIT_INTERFACE_MAP(WindowBaseImpl) @@ -45,8 +28,6 @@ BEGIN_INTERFACE_MAP() virtual HRESULT SetEnabled (bool enable) override; - virtual HRESULT SetParent (IAvnWindow* parent) override; - void StartStateTransition () override ; void EndStateTransition () override ; @@ -103,12 +84,23 @@ BEGIN_INTERFACE_MAP() protected: virtual NSWindowStyleMask CalculateStyleMask() override; - void UpdateStyle () override; + virtual void UpdateAppearance() override; private: void ZOrderChildWindows(); void OnInitialiseNSWindow(); NSString *_lastTitle; + bool _isEnabled; + bool _canResize; + bool _fullScreenActive; + SystemDecorations _decorations; + AvnWindowState _lastWindowState; + AvnWindowState _actualWindowState; + bool _inSetWindowState; + NSRect _preZoomSize; + bool _transitioningWindowState; + bool _isClientAreaExtended; + AvnExtendClientAreaChromeHints _extendClientHints; }; #endif //AVALONIA_NATIVE_OSX_WINDOWIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 1cdf81e2fb..42ac37ae8c 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -8,10 +8,10 @@ #include "AvnView.h" #include "automation.h" #include "WindowProtocol.h" +#include "WindowImpl.h" -WindowImpl::WindowImpl(IAvnWindowEvents *events) : WindowBaseImpl(events) { +WindowImpl::WindowImpl(IAvnWindowEvents *events) : TopLevelImpl(events), WindowBaseImpl(events, false) { _isEnabled = true; - _children = std::list(); _isClientAreaExtended = false; _extendClientHints = AvnDefaultChrome; _fullScreenActive = false; @@ -22,7 +22,7 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events) : WindowBaseImpl(events) { _lastWindowState = Normal; _actualWindowState = Normal; _lastTitle = @""; - _parent = nullptr; + Parent = nullptr; WindowEvents = events; [Window setHasShadow:true]; @@ -69,40 +69,7 @@ HRESULT WindowImpl::SetEnabled(bool enable) { @autoreleasepool { _isEnabled = enable; [GetWindowProtocol() setEnabled:enable]; - UpdateStyle(); - return S_OK; - } -} - -HRESULT WindowImpl::SetParent(IAvnWindow *parent) { - START_COM_CALL; - - @autoreleasepool { - if(_parent != nullptr) - { - _parent->_children.remove(this); - } - - auto cparent = dynamic_cast(parent); - - _parent = cparent; - - _isModal = _parent != nullptr; - - if(_parent != nullptr && Window != nullptr){ - // If one tries to show a child window with a minimized parent window, then the parent window will be - // restored but macOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive - // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. - if (cparent->WindowState() == Minimized) - cparent->SetWindowState(Normal); - - [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; - - cparent->_children.push_back(this); - - UpdateStyle(); - } - + UpdateAppearance(); return S_OK; } } @@ -156,12 +123,12 @@ bool WindowImpl::CanBecomeKeyWindow() void WindowImpl::StartStateTransition() { _transitioningWindowState = true; - UpdateStyle(); + UpdateAppearance(); } void WindowImpl::EndStateTransition() { _transitioningWindowState = false; - UpdateStyle(); + UpdateAppearance(); // Ensure correct order of child windows after fullscreen transition. ZOrderChildWindows(); @@ -236,7 +203,7 @@ HRESULT WindowImpl::SetCanResize(bool value) { @autoreleasepool { _canResize = value; - UpdateStyle(); + UpdateAppearance(); return S_OK; } } @@ -252,7 +219,7 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) { return S_OK; } - UpdateStyle(); + UpdateAppearance(); switch (_decorations) { case SystemDecorationsNone: @@ -427,7 +394,7 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { } [GetWindowProtocol() setIsExtended:enable]; - UpdateStyle(); + UpdateAppearance(); } return S_OK; @@ -579,7 +546,7 @@ bool WindowImpl::IsModal() { } bool WindowImpl::IsOwned() { - return _parent != nullptr; + return Parent != nullptr; } NSWindowStyleMask WindowImpl::CalculateStyleMask() { @@ -620,8 +587,8 @@ NSWindowStyleMask WindowImpl::CalculateStyleMask() { return s; } -void WindowImpl::UpdateStyle() { - WindowBaseImpl::UpdateStyle(); +void WindowImpl::UpdateAppearance() { + WindowBaseImpl::UpdateAppearance(); if (Window == nil) { return; @@ -642,3 +609,12 @@ void WindowImpl::UpdateStyle() { [zoomButton setHidden:!hasTrafficLights]; [zoomButton setEnabled:CanZoom()]; } + +extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events) +{ + @autoreleasepool + { + IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events); + return ptr; + } +} diff --git a/native/Avalonia.Native/src/OSX/WindowInterfaces.h b/native/Avalonia.Native/src/OSX/WindowInterfaces.h index 6e6d62e85e..d920183435 100644 --- a/native/Avalonia.Native/src/OSX/WindowInterfaces.h +++ b/native/Avalonia.Native/src/OSX/WindowInterfaces.h @@ -7,11 +7,13 @@ #import #include "WindowProtocol.h" #include "WindowBaseImpl.h" +#include "AvnAccessibility.h" -@interface AvnWindow : NSWindow +@interface AvnWindow : NSWindow -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; +-(AvnView* _Nullable) view; @end -@interface AvnPanel : NSPanel +@interface AvnPanel : NSPanel -(AvnPanel* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; -@end \ No newline at end of file +@end diff --git a/native/Avalonia.Native/src/OSX/WindowProtocol.h b/native/Avalonia.Native/src/OSX/WindowProtocol.h index cb5f86bdb9..5d1df951a6 100644 --- a/native/Avalonia.Native/src/OSX/WindowProtocol.h +++ b/native/Avalonia.Native/src/OSX/WindowProtocol.h @@ -8,6 +8,7 @@ #import @class AvnMenu; +struct IAvnAutomationPeer; @protocol AvnWindowProtocol -(void) pollModalSession: (NSModalSession _Nonnull) session; @@ -16,6 +17,7 @@ -(void) showAppMenuOnly; -(void) showWindowMenuWithAppMenu; -(void) applyMenu:(AvnMenu* _Nullable)menu; +-(IAvnAutomationPeer* _Nonnull) automationPeer; -(double) getExtendedTitleBarHeight; -(void) setIsExtended:(bool)value; diff --git a/native/Avalonia.Native/src/OSX/automation.h b/native/Avalonia.Native/src/OSX/automation.h index 367df3619d..5d96637241 100644 --- a/native/Avalonia.Native/src/OSX/automation.h +++ b/native/Avalonia.Native/src/OSX/automation.h @@ -1,12 +1,13 @@ #pragma once #import +#include "AvnAccessibility.h" NS_ASSUME_NONNULL_BEGIN class IAvnAutomationPeer; -@interface AvnAccessibilityElement : NSAccessibilityElement -+ (AvnAccessibilityElement *) acquire:(IAvnAutomationPeer *) peer; +@interface AvnAccessibilityElement : NSAccessibilityElement ++ (id _Nullable) acquire:(IAvnAutomationPeer *) peer; @end NS_ASSUME_NONNULL_END diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 9fe0ff3c60..7171de38f7 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -1,66 +1,19 @@ #include "common.h" #include "automation.h" +#include "AvnAutomationNode.h" #include "AvnString.h" #include "INSWindowHolder.h" #include "AvnView.h" - -@interface AvnAccessibilityElement (Events) -- (void) raiseChildrenChanged; -@end - -@interface AvnRootAccessibilityElement : AvnAccessibilityElement -- (AvnView *) ownerView; -- (AvnRootAccessibilityElement *) initWithPeer:(IAvnAutomationPeer *) peer owner:(AvnView*) owner; -- (void) raiseFocusChanged; -@end - -class AutomationNode : public ComSingleObject -{ -public: - FORWARD_IUNKNOWN() - - AutomationNode(AvnAccessibilityElement* owner) - { - _owner = owner; - } - - AvnAccessibilityElement* GetOwner() - { - return _owner; - } - - virtual void Dispose() override - { - _owner = nil; - } - - virtual void ChildrenChanged () override - { - [_owner raiseChildrenChanged]; - } - - virtual void PropertyChanged (AvnAutomationProperty property) override - { - - } - - virtual void FocusChanged () override - { - [(AvnRootAccessibilityElement*)_owner raiseFocusChanged]; - } - -private: - __strong AvnAccessibilityElement* _owner; -}; +#include "WindowInterfaces.h" @implementation AvnAccessibilityElement { IAvnAutomationPeer* _peer; - AutomationNode* _node; + AvnAutomationNode* _node; NSMutableArray* _children; } -+ (AvnAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer ++ (NSAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer { if (peer == nullptr) return nil; @@ -68,9 +21,14 @@ private: auto instance = peer->GetNode(); if (instance != nullptr) - return dynamic_cast(instance)->GetOwner(); + return dynamic_cast(instance)->GetOwner(); - if (peer->IsRootProvider()) + if (peer->IsInteropPeer()) + { + auto view = (__bridge NSAccessibilityElement*)peer->InteropPeer_GetNativeControlHandle(); + return view; + } + else if (peer->IsRootProvider()) { auto window = peer->RootProvider_GetWindow(); @@ -80,9 +38,9 @@ private: return nil; } - auto holder = dynamic_cast(window); + auto holder = dynamic_cast(window); auto view = holder->GetNSView(); - return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view]; + return (NSAccessibilityElement*)[view window]; } else { @@ -94,7 +52,7 @@ private: { self = [super init]; _peer = peer; - _node = new AutomationNode(self); + _node = new AvnAutomationNode(self); _peer->SetNode(_node); return self; } @@ -256,25 +214,8 @@ private: - (NSRect)accessibilityFrame { - id topLevel = [self accessibilityTopLevelUIElement]; - auto result = NSZeroRect; - - if ([topLevel isKindOfClass:[AvnRootAccessibilityElement class]]) - { - auto root = (AvnRootAccessibilityElement*)topLevel; - auto view = [root ownerView]; - - if (view) - { - auto window = [view window]; - auto bounds = ToNSRect(_peer->GetBoundingRectangle()); - auto windowBounds = [view convertRect:bounds toView:nil]; - auto screenBounds = [window convertRectToScreen:windowBounds]; - result = screenBounds; - } - } - - return result; + auto bounds = _peer->GetBoundingRectangle(); + return [self rectToScreen:bounds]; } - (id)accessibilityParent @@ -389,6 +330,24 @@ private: return [super isAccessibilitySelectorAllowed:selector]; } +- (NSRect)rectToScreen:(AvnRect)rect +{ + id topLevel = [self accessibilityTopLevelUIElement]; + + if (![topLevel isKindOfClass:[AvnWindow class]]) + return NSZeroRect; + + auto window = (AvnWindow*)topLevel; + auto view = [window view]; + + if (view == nil) + return NSZeroRect; + + auto nsRect = ToNSRect(rect); + auto windowRect = [view convertRect:nsRect toView:nil]; + return [window convertRectToScreen:windowRect]; +} + - (void)raiseChildrenChanged { auto changed = _children ? [NSMutableSet setWithArray:_children] : [NSMutableSet set]; @@ -429,7 +388,7 @@ private: if (childPeers->Get(i, &child) == S_OK) { - auto element = [AvnAccessibilityElement acquire:child]; + id element = [AvnAccessibilityElement acquire:child]; [_children addObject:element]; } } @@ -441,64 +400,3 @@ private: } @end - -@implementation AvnRootAccessibilityElement -{ - AvnView* _owner; -} - -- (AvnRootAccessibilityElement *)initWithPeer:(IAvnAutomationPeer *)peer owner:(AvnView *)owner -{ - self = [super initWithPeer:peer]; - _owner = owner; - - // Seems we need to raise a focus changed notification here if we have focus - auto focusedPeer = [self peer]->RootProvider_GetFocus(); - id focused = [AvnAccessibilityElement acquire:focusedPeer]; - - if (focused) - NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification); - - return self; -} - -- (AvnView *)ownerView -{ - return _owner; -} - -- (id)accessibilityFocusedUIElement -{ - auto focusedPeer = [self peer]->RootProvider_GetFocus(); - return [AvnAccessibilityElement acquire:focusedPeer]; -} - -- (id)accessibilityHitTest:(NSPoint)point -{ - auto clientPoint = [[_owner window] convertPointFromScreen:point]; - auto localPoint = [_owner translateLocalPoint:ToAvnPoint(clientPoint)]; - auto hit = [self peer]->RootProvider_GetPeerFromPoint(localPoint); - return [AvnAccessibilityElement acquire:hit]; -} - -- (id)accessibilityParent -{ - return _owner; -} - -- (void)raiseFocusChanged -{ - id focused = [self accessibilityFocusedUIElement]; - NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification); -} - -// Although this method is marked as deprecated we get runtime warnings if we don't handle it. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-implementations" -- (void)accessibilityPerformAction:(NSAccessibilityActionName)action -{ - [_owner accessibilityPerformAction:action]; -} -#pragma clang diagnostic pop - -@end diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index a3473d3928..36c157704d 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -11,10 +11,11 @@ extern IAvnPlatformThreadingInterface* CreatePlatformThreading(); extern void FreeAvnGCHandle(void* handle); extern void PostDispatcherCallback(IAvnActionCallback* cb); +extern IAvnTopLevel* CreateAvnTopLevel(IAvnTopLevelEvents* events); extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events); extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events); -extern IAvnSystemDialogs* CreateSystemDialogs(); -extern IAvnScreens* CreateScreens(); +extern IAvnStorageProvider* CreateStorageProvider(); +extern IAvnScreens* CreateScreens(IAvnScreenEvents* cb); extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*); extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*); extern NSObject* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle); @@ -46,6 +47,7 @@ extern NSRect ToNSRect (AvnRect r); extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ConvertPointY (AvnPoint p); extern NSSize ToNSSize (AvnSize s); +extern AvnSize FromNSSize (NSSize s); #ifdef DEBUG #define NSDebugLog(...) NSLog(__VA_ARGS__) #else @@ -87,6 +89,13 @@ public: - (void) action; @end +@implementation NSScreen (AvNSScreen) +- (CGDirectDisplayID)av_displayId +{ + return [self.deviceDescription[@"NSScreenNumber"] unsignedIntValue]; +} +@end + class AvnInsidePotentialDeadlock { public: diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 41d6fd37ab..d1dbe9d186 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -225,6 +225,19 @@ public: return (IAvnMacOptions*)new MacOptions(); } + virtual HRESULT CreateTopLevel(IAvnTopLevelEvents* cb, + IAvnTopLevel** ppv) override { + START_COM_CALL; + + @autoreleasepool + { + if(cb == nullptr || ppv == nullptr) + return E_POINTER; + *ppv = CreateAvnTopLevel(cb); + return S_OK; + } + } + virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnWindow** ppv) override { START_COM_CALL; @@ -263,24 +276,24 @@ public: } } - virtual HRESULT CreateSystemDialogs(IAvnSystemDialogs** ppv) override + virtual HRESULT CreateStorageProvider(IAvnStorageProvider** ppv) override { START_COM_CALL; @autoreleasepool { - *ppv = ::CreateSystemDialogs(); + *ppv = ::CreateStorageProvider(); return S_OK; } } - virtual HRESULT CreateScreens (IAvnScreens** ppv) override + virtual HRESULT CreateScreens (IAvnScreenEvents* cb, IAvnScreens** ppv) override { START_COM_CALL; @autoreleasepool { - *ppv = ::CreateScreens (); + *ppv = ::CreateScreens (cb); return S_OK; } } @@ -484,6 +497,15 @@ NSSize ToNSSize (AvnSize s) return result; } +AvnSize FromNSSize (NSSize s) +{ + AvnSize result; + result.Width = s.width; + result.Height = s.height; + + return result; +} + NSPoint ToNSPoint (AvnPoint p) { NSPoint result; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 7a7edcb1cb..1235979cb2 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -1,4 +1,5 @@ + #include "common.h" #include "menu.h" #include "KeyTransform.h" diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 9157e4c44b..c71a0c1e39 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -19,6 +19,7 @@ using static Nuke.Common.Tools.Xunit.XunitTasks; using static Nuke.Common.Tools.VSWhere.VSWhereTasks; using static Serilog.Log; using MicroCom.CodeGenerator; +using NuGet.Configuration; using Nuke.Common.IO; /* @@ -39,7 +40,8 @@ partial class Build : NukeBuild [PackageExecutable("Microsoft.DotNet.GenAPI.Tool", "Microsoft.DotNet.GenAPI.Tool.dll", Framework = "net8.0")] Tool ApiGenTool; - + [PackageExecutable("dotnet-ilrepack", "ILRepackTool.dll", Framework = "net8.0")] + Tool IlRepackTool; protected override void OnBuildInitialized() { @@ -306,7 +308,8 @@ partial class Build : NukeBuild .Executes(() => { BuildTasksPatcher.PatchBuildTasksInPackage(Parameters.NugetIntermediateRoot / "Avalonia.Build.Tasks." + - Parameters.Version + ".nupkg"); + Parameters.Version + ".nupkg", + IlRepackTool); var config = Numerge.MergeConfiguration.LoadFile(RootDirectory / "nukebuild" / "numerge.config"); EnsureCleanDirectory(Parameters.NugetRoot); if(!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, @@ -366,6 +369,9 @@ partial class Build : NukeBuild { if (!Parameters.IsPackingToLocalCache) throw new InvalidOperationException(); + + var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder( + Settings.LoadDefaultSettings(RootDirectory)); foreach (var path in Parameters.NugetRoot.GlobFiles("*.nupkg")) { @@ -376,11 +382,11 @@ partial class Build : NukeBuild .Elements().First(x => x.Name.LocalName == "metadata") .Elements().First(x => x.Name.LocalName == "id").Value; - var packagePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".nuget", - "packages", + var packagePath = Path.Combine( + globalPackagesFolder, packageId.ToLowerInvariant(), BuildParameters.LocalBuildVersion); + if (Directory.Exists(packagePath)) Directory.Delete(packagePath, true); Directory.CreateDirectory(packagePath); diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index d664d74d2a..50f4f1c5da 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -116,9 +116,10 @@ public partial class Build IsNuGetRelease = IsMainRepo && IsReleasable && IsReleaseBranch; // VERSION - Version = b.ForceNugetVersion ?? GetVersion(); + var (propsVersion, propsApiCompatVersion) = GetVersion(); + Version = b.ForceNugetVersion ?? propsVersion; - ApiValidationBaseline = b.ApiValidationBaseline ?? new Version(new Version(Version.Split('-', StringSplitOptions.None).First()).Major, 0).ToString(); + ApiValidationBaseline = b.ApiValidationBaseline ?? propsApiCompatVersion; UpdateApiValidationSuppression = b.UpdateApiValidationSuppression ?? IsLocalBuild; if (IsRunningOnAzure) @@ -153,10 +154,13 @@ public partial class Build VersionOutputDir = b.VersionOutputDir; } - string GetVersion() + (string Version, string ApiCompatVersion) GetVersion() { var xdoc = XDocument.Load(RootDirectory / "build/SharedVersion.props"); - return xdoc.Descendants().First(x => x.Name.LocalName == "Version").Value; + return ( + xdoc.Descendants().First(x => x.Name.LocalName == "Version").Value, + xdoc.Descendants().First(x => x.Name.LocalName == "ApiCompatVersion").Value + ); } } diff --git a/nukebuild/BuildTasksPatcher.cs b/nukebuild/BuildTasksPatcher.cs index f2dd217657..6bb71f4320 100644 --- a/nukebuild/BuildTasksPatcher.cs +++ b/nukebuild/BuildTasksPatcher.cs @@ -2,9 +2,9 @@ using System; using System.IO; using System.IO.Compression; using System.Linq; -using ILRepacking; using Mono.Cecil; using Mono.Cecil.Cil; +using Nuke.Common.Tooling; public class BuildTasksPatcher { @@ -56,7 +56,7 @@ public class BuildTasksPatcher return null; } - public static void PatchBuildTasksInPackage(string packagePath) + public static void PatchBuildTasksInPackage(string packagePath, Tool ilRepackTool) { using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), ZipArchiveMode.Update)) @@ -70,7 +70,7 @@ public class BuildTasksPatcher Directory.CreateDirectory(tempDir); var temp = Path.Combine(tempDir, entry.Name); var output = temp + ".output"; - File.Copy(typeof(Microsoft.Build.Framework.ITask).Assembly.GetModules()[0].FullyQualifiedName, + File.Copy(GetAssemblyPath(typeof(Microsoft.Build.Framework.ITask)), Path.Combine(tempDir, "Microsoft.Build.Framework.dll")); var patched = new MemoryStream(); try @@ -78,22 +78,15 @@ public class BuildTasksPatcher entry.ExtractToFile(temp, true); // Get Original SourceLinkInfo Content var sourceLinkInfoContent = GetSourceLinkInfo(temp); - var repack = new ILRepacking.ILRepack(new RepackOptions() - { - Internalize = true, - InputAssemblies = new[] - { - temp, - typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0].FullyQualifiedName, - typeof(Mono.Cecil.Rocks.MethodBodyRocks).Assembly.GetModules()[0].FullyQualifiedName, - typeof(Mono.Cecil.Pdb.PdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName, - typeof(Mono.Cecil.Mdb.MdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName, - }, - SearchDirectories = Array.Empty(), - DebugInfo = true, // Allowed read debug info - OutputFile = output - }); - repack.Repack(); + + var cecilAsm = GetAssemblyPath(typeof(Mono.Cecil.AssemblyDefinition)); + var cecilRocksAsm = GetAssemblyPath(typeof(Mono.Cecil.Rocks.MethodBodyRocks)); + var cecilPdbAsm = GetAssemblyPath(typeof(Mono.Cecil.Pdb.PdbReaderProvider)); + var cecilMdbAsm = GetAssemblyPath(typeof(Mono.Cecil.Mdb.MdbReaderProvider)); + + ilRepackTool.Invoke( + $"/internalize /out:\"{output}\" \"{temp}\" \"{cecilAsm}\" \"{cecilRocksAsm}\" \"{cecilPdbAsm}\" \"{cecilMdbAsm}\"", + tempDir); // 'hurr-durr assembly with the same name is already loaded' prevention using (var asm = AssemblyDefinition.ReadAssembly(output, @@ -161,4 +154,7 @@ public class BuildTasksPatcher } } } + + private static string GetAssemblyPath(Type typeInAssembly) + => typeInAssembly.Assembly.GetModules()[0].FullyQualifiedName; } diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 7c89b896c7..a2c4f890da 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -20,7 +20,6 @@ - all @@ -29,6 +28,7 @@ + @@ -38,9 +38,7 @@ - - diff --git a/nukebuild/_build.csproj.DotSettings b/nukebuild/_build.csproj.DotSettings index 9aac7d8e8d..7348ae5acb 100644 --- a/nukebuild/_build.csproj.DotSettings +++ b/nukebuild/_build.csproj.DotSettings @@ -13,6 +13,8 @@ False <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> True True True @@ -21,4 +23,5 @@ True True True - True + True + True diff --git a/nukebuild/il-repack b/nukebuild/il-repack deleted file mode 160000 index 892f079ea8..0000000000 --- a/nukebuild/il-repack +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 892f079ea8cb0c178f0a68f53a7a7eac13acdda9 diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 99b3df9b1a..9d68af031f 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -135,6 +135,14 @@ Outputs="@(CompileAvaloniaXamlOutputs)" Condition="'@(AvaloniaResource)@(AvaloniaXaml)' != '' AND $(DesignTimeBuild) != true AND $(EnableAvaloniaXamlCompilation) != false"> + + + - + + Condition="$([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '9.0')) AND '@(AvaloniaResource)@(AvaloniaXaml)' != '' AND $(EnableAvaloniaXamlCompilation) != false"> @@ -241,22 +253,4 @@ '$(SkipCopyBuildProduct)' != 'true'"> - - - - - $(IntermediateOutputPath)/Avalonia/references - - - diff --git a/readme.md b/readme.md index 30c5be7675..53db867b46 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,7 @@ -![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) + +![Star Banner](https://github.com/AvaloniaUI/Avalonia/assets/552074/0f7f683f-2ddd-401f-ba28-3f703cc78ee0) +![Header](https://github.com/AvaloniaUI/Avalonia/assets/552074/d8388fc4-469e-47c5-926d-faf25233ad4e) + [![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) [![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://img.shields.io/opencollective/backers/Avalonia?logo=opencollective)](#backers) [![Sponsors on Open Collective](https://img.shields.io/opencollective/sponsors/Avalonia?logo=opencollective)](#sponsors) [![GitHub Sponsors](https://img.shields.io/github/sponsors/AvaloniaUI?logo=github)](https://github.com/sponsors/AvaloniaUI) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg) @@ -45,9 +47,7 @@ Install-Package Avalonia.Desktop ``` ## Showcase -[![Showcase_Banner](https://user-images.githubusercontent.com/552074/235946124-bf6fda52-0c9f-4730-868b-0de957e5b97b.png)](https://avaloniaui.net/showcase) - - +[![Showcase_Banner@3x](https://github.com/AvaloniaUI/Avalonia/assets/552074/8a0af0e9-e45e-442c-830d-4af3767d6469)](https://avaloniaui.net/showcase) See what others have built with Avalonia UI on our [Showcase](https://avaloniaui.net/Showcase). We welcome submissions! @@ -102,12 +102,6 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com -### Sponsors - -Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/Avalonia#sponsor)] - - - ## Commercial Support We have a range of [support plans available](https://avaloniaui.net/support) for those looking to partner with the creators of Avalonia, enabling access to the best support at every step of the development process. diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index dcc2066149..c54b616b17 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -1,5 +1,6 @@ using Android.App; using Android.Content.PM; +using Android.OS; using Avalonia; using Avalonia.Android; using static Android.Content.Intent; @@ -10,10 +11,9 @@ using static Android.Content.Intent; namespace ControlCatalog.Android { - [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, Exported = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] - // CategoryBrowsable and DataScheme are required for Protocol activation. + [Activity(Name = "com.Avalonia.ControlCatalog.MainActivity", Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, Exported = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] // CategoryLeanbackLauncher is required for Android TV. - [IntentFilter(new [] { ActionView }, Categories = new [] { CategoryDefault, CategoryBrowsable, CategoryLeanbackLauncher }, DataScheme = "avln" )] + [IntentFilter(new [] { ActionView }, Categories = new [] { CategoryDefault, CategoryLeanbackLauncher })] public class MainActivity : AvaloniaMainActivity { protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) @@ -25,4 +25,19 @@ namespace ControlCatalog.Android }); } } + + /// + /// Special activity to handle OpenUri activation. + /// `AvaloniaActivity` internally will redirect parameters to the Avalonia Application. + /// + [Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true, Theme = "@android:style/Theme.NoDisplay")] + [IntentFilter(new[] {ActionView}, Categories = new[] {CategoryDefault, CategoryBrowsable}, DataScheme = "avln")] + public class DataSchemeActivity : AvaloniaActivity + { + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + Finish(); + } + } } diff --git a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj index f223eb0725..b921945f25 100644 --- a/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj +++ b/samples/ControlCatalog.Browser/ControlCatalog.Browser.csproj @@ -1,10 +1,10 @@ - + $(AvsCurrentBrowserTargetFramework) - browser-wasm - wwwroot/main.js - Exe + false true + true + 5 @@ -12,9 +12,12 @@ - - + + diff --git a/samples/ControlCatalog.Browser/EmbedSample.Browser.cs b/samples/ControlCatalog.Browser/EmbedSample.Browser.cs index 7bf3891a81..b8bc4ed35c 100644 --- a/samples/ControlCatalog.Browser/EmbedSample.Browser.cs +++ b/samples/ControlCatalog.Browser/EmbedSample.Browser.cs @@ -29,7 +29,7 @@ public class EmbedSampleWeb : INativeDemoControl static async void AddButton(JSObject parent) { - await JSHost.ImportAsync("embed.js", "./embed.js"); + await JSHost.ImportAsync("embed.js", "../embed.js"); EmbedInterop.AddAppButton(parent); } } diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs index c50f1dcbdd..95cce73eb3 100644 --- a/samples/ControlCatalog.Browser/Program.cs +++ b/samples/ControlCatalog.Browser/Program.cs @@ -33,10 +33,13 @@ internal partial class Program }) .StartBrowserAppAsync("out", options); - if (Application.Current!.ApplicationLifetime is ISingleTopLevelApplicationLifetime lifetime) + Dispatcher.UIThread.Invoke(() => { - lifetime.TopLevel!.RendererDiagnostics.DebugOverlays = RendererDebugOverlays.Fps; - } + if (Application.Current!.ApplicationLifetime is ISingleTopLevelApplicationLifetime lifetime) + { + lifetime.TopLevel!.RendererDiagnostics.DebugOverlays = RendererDebugOverlays.Fps; + } + }); } // Test with multiple AvaloniaView at once. diff --git a/samples/ControlCatalog.Browser/wwwroot/Logo.svg b/samples/ControlCatalog.Browser/wwwroot/Logo.svg deleted file mode 100644 index 3e18ea1958..0000000000 --- a/samples/ControlCatalog.Browser/wwwroot/Logo.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/samples/ControlCatalog.Browser/wwwroot/app.css b/samples/ControlCatalog.Browser/wwwroot/app.css index 77f2051221..f27738d8ea 100644 --- a/samples/ControlCatalog.Browser/wwwroot/app.css +++ b/samples/ControlCatalog.Browser/wwwroot/app.css @@ -3,8 +3,8 @@ position: absolute; height: 100%; width: 100%; - background: #1b2a4e; - font-family: 'Nunito', sans-serif; + background: white; + font-family: 'Outfit', sans-serif; justify-content: center; align-items: center; display: flex; @@ -12,23 +12,14 @@ } .avalonia-splash h2 { + font-weight: 400; font-size: 1.5rem; - color: #8b44ac; } .avalonia-splash a { - color: white; text-decoration: none; font-size: 2.5rem; - display: block; -} - -.avalonia-splash img { - opacity: 0.05; - height: 35%; - position: absolute; - right: 3%; - bottom: 3%; + display: block; } .avalonia-splash.splash-close { @@ -36,3 +27,32 @@ display: none; opacity: 0; } + +/* Light theme styles */ +@media (prefers-color-scheme: light) { + .avalonia-splash { + background: white; + } + + .avalonia-splash h2 { + color: #1b2a4e; + } + + .avalonia-splash a { + color: #0D6EFD; + } +} + +@media (prefers-color-scheme: dark) { + .avalonia-splash { + background: #1b2a4e; + } + + .avalonia-splash h2 { + color: white; + } + + .avalonia-splash a { + color: white; + } +} diff --git a/samples/ControlCatalog.Browser/wwwroot/index.html b/samples/ControlCatalog.Browser/wwwroot/index.html index d8bf05fe3c..4a1e12bbc1 100644 --- a/samples/ControlCatalog.Browser/wwwroot/index.html +++ b/samples/ControlCatalog.Browser/wwwroot/index.html @@ -15,9 +15,21 @@ diff --git a/samples/ControlCatalog.Browser/wwwroot/main.js b/samples/ControlCatalog.Browser/wwwroot/main.js index 35c8245b01..a674ae6ae8 100644 --- a/samples/ControlCatalog.Browser/wwwroot/main.js +++ b/samples/ControlCatalog.Browser/wwwroot/main.js @@ -1,6 +1,3 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - import { dotnet } from './_framework/dotnet.js' const is_browser = typeof window != "undefined"; diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index d555ce1399..533ee8308a 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -1,7 +1,5 @@ using System; -using System.Linq; using Avalonia; -using Avalonia.Controls; using Avalonia.Platform; using ControlCatalog.NetCore; using ControlCatalog.Pages; @@ -29,7 +27,8 @@ namespace ControlCatalog EmbedSample.Implementation = new EmbedSampleWin(); }) - .UsePlatformDetect(); + .UseWin32() + .UseSkia(); private static void ConfigureAssetAssembly(AppBuilder builder) { diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 18d222d809..edcbcbe988 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -30,10 +30,12 @@ + + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 021a29de01..600cc66fa7 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -130,6 +130,9 @@ + + + @@ -196,6 +199,9 @@ + + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 31fa54a23a..89bccb4475 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -25,16 +25,6 @@ namespace ControlCatalog var sideBar = this.Get("Sidebar"); - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime) - { - var tabItems = (sideBar.Items as IList); - tabItems?.Add(new TabItem() - { - Header = "Screens", - Content = new ScreenPage() - }); - } - var themes = this.Get("Themes"); themes.SelectedItem = App.CurrentTheme; themes.SelectionChanged += (sender, e) => diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 88252091c4..a968b50666 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -45,6 +45,11 @@ + + + diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs index 617b0db5ed..149b36fc3f 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs @@ -24,7 +24,6 @@ namespace ControlCatalog.Pages collectionView1.SortDescriptions.Add(dataGridSortDescription); var dg1 = this.Get("dataGrid1"); dg1.IsReadOnly = true; - dg1.LoadingRow += Dg1_LoadingRow; dg1.Sorting += (s, a) => { var binding = (a.Column as DataGridBoundColumn)?.Binding as Binding; @@ -64,11 +63,6 @@ namespace ControlCatalog.Pages } public IEnumerable DataGrid3Source { get; } - - private void Dg1_LoadingRow(object? sender, DataGridRowEventArgs e) - { - e.Row.Header = e.Row.GetIndex() + 1; - } private void InitializeComponent() { diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml index fc3ad9b895..7a1bfaa824 100644 --- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml @@ -77,6 +77,23 @@ + A TimePicker with seconds enabled. + + + + + + + + + <TimePicker UseSeconds="True" /> + + + + + + @@ -85,8 +102,8 @@ - - A TimePicker with minute increments specified. + + A TimePicker with minute increment specified. @@ -96,7 +113,24 @@ - <TimePicker MinuteIncrement="15" /> + <TimePicker MinuteIncrement="15" SecondIncrement="30" /> + + + + + + + A TimePicker with seconds enabled and minute & second increments specified. + + + + + + + + + <TimePicker UseSeconds="True" MinuteIncrement="15" SecondIncrement="30" /> @@ -137,6 +171,40 @@ + A TimePicker using a 12-hour clock and seconds. + + + + + + + + + <TimePicker ClockIdentifier="12HourClock" UseSeconds="True" /> + + + + + + + A TimePicker using a 24-hour clock and seconds. + + + + + + + + + <TimePicker ClockIdentifier="24HourClock" UseSeconds="True" /> + + + + + + diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs index 7520dabf37..5c7ccc151b 100644 --- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs @@ -15,7 +15,7 @@ namespace ControlCatalog.Pages "Order of month, day, and year is dynamically set based on user date settings"; this.Get("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " + - "to set a reminder. The TimePicker displays three controls for hour, minute, and AM / PM(if necessary).These controls " + + "to set a reminder. The TimePicker displays four controls for hour, minute, seconds(optional), and AM / PM(if necessary).These controls " + "are easy to use with touch or mouse, and they can be styled and configured in several different ways. " + "12 - hour or 24 - hour clock and visibility of AM / PM is dynamically set based on user time settings, or can be overridden."; diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 7cfb036577..5b7814fb5d 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -1,9 +1,11 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Security; +using System.Text; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; @@ -252,17 +254,24 @@ namespace ControlCatalog.Pages if (file is not null) { - // Sync disposal of StreamWriter is not supported on WASM + try + { + // Sync disposal of StreamWriter is not supported on WASM #if NET6_0_OR_GREATER - await using var stream = await file.OpenWriteAsync(); - await using var reader = new System.IO.StreamWriter(stream); + await using var stream = await file.OpenWriteAsync(); + await using var writer = new System.IO.StreamWriter(stream); #else - using var stream = await file.OpenWriteAsync(); - using var reader = new System.IO.StreamWriter(stream); + using var stream = await file.OpenWriteAsync(); + using var writer = new System.IO.StreamWriter(stream); #endif - await reader.WriteLineAsync(openedFileContent.Text); + await writer.WriteLineAsync(openedFileContent.Text); - SetFolder(await file.GetParentAsync()); + SetFolder(await file.GetParentAsync()); + } + catch (Exception ex) + { + openedFileContent.Text = ex.ToString(); + } } await SetPickerResult(file is null ? null : new[] { file }); @@ -278,8 +287,6 @@ namespace ControlCatalog.Pages }); await SetPickerResult(folders); - - SetFolder(folders.FirstOrDefault()); }; this.Get diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs index c77d65ddf1..f7a7e60f89 100644 --- a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs @@ -1,375 +1,84 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Numerics; using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; using Avalonia.OpenGL; using Avalonia.OpenGL.Controls; -using Avalonia.Platform.Interop; -using Avalonia.Threading; -using static Avalonia.OpenGL.GlConsts; +using Avalonia.Rendering.Composition; +using ControlCatalog.Pages.OpenGl; + // ReSharper disable StringLiteralTypo namespace ControlCatalog.Pages { public class OpenGlPage : UserControl { - - } - - public class OpenGlPageControl : OpenGlControlBase - { - private float _yaw; - - public static readonly DirectProperty YawProperty = - AvaloniaProperty.RegisterDirect("Yaw", o => o.Yaw, (o, v) => o.Yaw = v); - - public float Yaw - { - get => _yaw; - set => SetAndRaise(YawProperty, ref _yaw, value); - } - - private float _pitch; - - public static readonly DirectProperty PitchProperty = - AvaloniaProperty.RegisterDirect("Pitch", o => o.Pitch, (o, v) => o.Pitch = v); - - public float Pitch - { - get => _pitch; - set => SetAndRaise(PitchProperty, ref _pitch, value); - } - - - private float _roll; - - public static readonly DirectProperty RollProperty = - AvaloniaProperty.RegisterDirect("Roll", o => o.Roll, (o, v) => o.Roll = v); - - public float Roll - { - get => _roll; - set => SetAndRaise(RollProperty, ref _roll, value); - } - - - private float _disco; - - public static readonly DirectProperty DiscoProperty = - AvaloniaProperty.RegisterDirect("Disco", o => o.Disco, (o, v) => o.Disco = v); - - public float Disco - { - get => _disco; - set => SetAndRaise(DiscoProperty, ref _disco, value); - } - - private string _info = string.Empty; - - public static readonly DirectProperty InfoProperty = - AvaloniaProperty.RegisterDirect("Info", o => o.Info, (o, v) => o.Info = v); - - public string Info - { - get => _info; - private set => SetAndRaise(InfoProperty, ref _info, value); - } - - private int _vertexShader; - private int _fragmentShader; - private int _shaderProgram; - private int _vertexBufferObject; - private int _indexBufferObject; - private int _vertexArrayObject; - - private string GetShader(bool fragment, string shader) - { - var version = (GlVersion.Type == GlProfileType.OpenGL ? - RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 150 : 120 : - 100); - var data = "#version " + version + "\n"; - if (GlVersion.Type == GlProfileType.OpenGLES) - data += "precision mediump float;\n"; - if (version >= 150) - { - shader = shader.Replace("attribute", "in"); - if (fragment) - shader = shader - .Replace("varying", "in") - .Replace("//DECLAREGLFRAG", "out vec4 outFragColor;") - .Replace("gl_FragColor", "outFragColor"); - else - shader = shader.Replace("varying", "out"); - } - - data += shader; - - return data; - } - - - private string VertexShaderSource => GetShader(false, @" - attribute vec3 aPos; - attribute vec3 aNormal; - uniform mat4 uModel; - uniform mat4 uProjection; - uniform mat4 uView; - - varying vec3 FragPos; - varying vec3 VecPos; - varying vec3 Normal; - uniform float uTime; - uniform float uDisco; - void main() + public OpenGlPage() { - float discoScale = sin(uTime * 10.0) / 10.0; - float distortionX = 1.0 + uDisco * cos(uTime * 20.0) / 10.0; + AvaloniaXamlLoader.Load(this); + this.FindControl("GL") + !.Init(this.FindControl("Knobs")!); - float scale = 1.0 + uDisco * discoScale; - - vec3 scaledPos = aPos; - scaledPos.x = scaledPos.x * distortionX; - - scaledPos *= scale; - gl_Position = uProjection * uView * uModel * vec4(scaledPos, 1.0); - FragPos = vec3(uModel * vec4(aPos, 1.0)); - VecPos = aPos; - Normal = normalize(vec3(uModel * vec4(aNormal, 1.0))); - } -"); - - private string FragmentShaderSource => GetShader(true, @" - varying vec3 FragPos; - varying vec3 VecPos; - varying vec3 Normal; - uniform float uMaxY; - uniform float uMinY; - uniform float uTime; - uniform float uDisco; - //DECLAREGLFRAG - - void main() - { - float y = (VecPos.y - uMinY) / (uMaxY - uMinY); - float c = cos(atan(VecPos.x, VecPos.z) * 20.0 + uTime * 40.0 + y * 50.0); - float s = sin(-atan(VecPos.z, VecPos.x) * 20.0 - uTime * 20.0 - y * 30.0); - - vec3 discoColor = vec3( - 0.5 + abs(0.5 - y) * cos(uTime * 10.0), - 0.25 + (smoothstep(0.3, 0.8, y) * (0.5 - c / 4.0)), - 0.25 + abs((smoothstep(0.1, 0.4, y) * (0.5 - s / 4.0)))); - - vec3 objectColor = vec3((1.0 - y), 0.40 + y / 4.0, y * 0.75 + 0.25); - objectColor = objectColor * (1.0 - uDisco) + discoColor * uDisco; - - float ambientStrength = 0.3; - vec3 lightColor = vec3(1.0, 1.0, 1.0); - vec3 lightPos = vec3(uMaxY * 2.0, uMaxY * 2.0, uMaxY * 2.0); - vec3 ambient = ambientStrength * lightColor; - - - vec3 norm = normalize(Normal); - vec3 lightDir = normalize(lightPos - FragPos); - - float diff = max(dot(norm, lightDir), 0.0); - vec3 diffuse = diff * lightColor; - - vec3 result = (ambient + diffuse) * objectColor; - gl_FragColor = vec4(result, 1.0); - - } -"); - - [StructLayout(LayoutKind.Sequential, Pack = 4)] - private struct Vertex - { - public Vector3 Position; - public Vector3 Normal; + AttachedToVisualTree += delegate + { + if (TopLevel.GetTopLevel(this) is Window) + this.FindControl - - - - - - - - - - Sample RadioButton - - Three States: Option 1 - Three States: Option 2 - - - - - - - Unchecked - Checked - ThreeState - - - - - - - Item 0 - Item 1 - - Wrap Selection - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - None - - - - - - - - - - - - NonOwned - Owned - Modal - - - Manual - CenterScreen - CenterOwner - - - Normal - Minimized - Maximized - FullScreen - - - None - BorderOnly - Full - - ExtendClientAreaToDecorationsHint - Can Resize - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 986eb920a3..ee7c81bf22 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,289 +1,99 @@ +using System; using System.Collections.Generic; -using System.Linq; using Avalonia; -using Avalonia.Automation; using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Primitives.PopupPositioning; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Markup.Xaml; -using Avalonia.Media; -using Avalonia.VisualTree; -using Microsoft.CodeAnalysis; +using IntegrationTestApp.Models; +using IntegrationTestApp.Pages; +using IntegrationTestApp.ViewModels; namespace IntegrationTestApp { - public class MainWindow : Window + public partial class MainWindow : Window { public MainWindow() { + // Set name in code behind, so source generator will ignore it. + Name = "MainWindow"; + InitializeComponent(); - InitializeViewMenu(); - InitializeGesturesTab(); - this.AttachDevTools(); - var overlayPopups = this.Get("AppOverlayPopups"); - overlayPopups.Text = Program.OverlayPopups ? "Overlay Popups" : "Native Popups"; + var viewModel = new MainWindowViewModel(CreatePages()); + InitializeViewMenu(viewModel.Pages); - AddHandler(Button.ClickEvent, OnButtonClick); - ListBoxItems = Enumerable.Range(0, 100).Select(x => "Item " + x).ToList(); - DataContext = this; + DataContext = viewModel; + AppOverlayPopups.Text = Program.OverlayPopups ? "Overlay Popups" : "Native Popups"; + PositionChanged += OnPositionChanged; } - public List ListBoxItems { get; } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } + private MainWindowViewModel? ViewModel => (MainWindowViewModel?)DataContext; - private void InitializeViewMenu() + private void InitializeViewMenu(IEnumerable pages) { - var mainTabs = this.Get("MainTabs"); var viewMenu = (NativeMenuItem?)NativeMenu.GetMenu(this)?.Items[1]; - foreach (var tabItem in mainTabs.Items.Cast()) + foreach (var page in pages) { var menuItem = new NativeMenuItem { - Header = (string?)tabItem.Header, - ToolTip = $"Tip:{(string?)tabItem.Header}", - IsChecked = tabItem.IsSelected, + Header = (string?)page.Name, + ToolTip = $"Tip:{(string?)page.Name}", ToggleType = NativeMenuItemToggleType.Radio, }; - menuItem.Click += (_, _) => tabItem.IsSelected = true; - viewMenu?.Menu?.Items.Add(menuItem); - } - } - - private void ShowWindow() - { - var sizeTextBox = this.GetControl("ShowWindowSize"); - var modeComboBox = this.GetControl("ShowWindowMode"); - var locationComboBox = this.GetControl("ShowWindowLocation"); - var stateComboBox = this.GetControl("ShowWindowState"); - var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null; - var systemDecorations = this.GetControl("ShowWindowSystemDecorations"); - var extendClientArea = this.GetControl("ShowWindowExtendClientAreaToDecorationsHint"); - var canResizeCheckBox = this.GetControl("ShowWindowCanResize"); - var owner = (Window)this.GetVisualRoot()!; - - var window = new ShowWindowTest - { - WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex, - CanResize = canResizeCheckBox.IsChecked ?? false, - }; - - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) - { - // Make sure the windows have unique names and AutomationIds. - var existing = lifetime.Windows.OfType().Count(); - if (existing > 0) + menuItem.Click += (_, _) => { - AutomationProperties.SetAutomationId(window, window.Name + (existing + 1)); - window.Title += $" {existing + 1}"; - } - } - - if (size.HasValue) - { - window.Width = size.Value.Width; - window.Height = size.Value.Height; - } - - sizeTextBox.Text = string.Empty; - window.ExtendClientAreaToDecorationsHint = extendClientArea.IsChecked ?? false; - window.SystemDecorations = (SystemDecorations)systemDecorations.SelectedIndex; - window.WindowState = (WindowState)stateComboBox.SelectedIndex; + if (ViewModel is { } viewModel) + viewModel.SelectedPage = page; + }; - switch (modeComboBox.SelectedIndex) - { - case 0: - window.Show(); - break; - case 1: - window.Show(owner); - break; - case 2: - window.ShowDialog(owner); - break; + viewMenu?.Menu?.Items.Add(menuItem); } } - private void ShowTransparentWindow() - { - // Show a background window to make sure the color behind the transparent window is - // a known color (green). - var backgroundWindow = new Window - { - Title = "Transparent Window Background", - Name = "TransparentWindowBackground", - Width = 300, - Height = 300, - Background = Brushes.Green, - WindowStartupLocation = WindowStartupLocation.CenterOwner, - }; - - // This is the transparent window with a red circle. - var window = new Window - { - Title = "Transparent Window", - Name = "TransparentWindow", - SystemDecorations = SystemDecorations.None, - Background = Brushes.Transparent, - TransparencyLevelHint = new[] { WindowTransparencyLevel.Transparent }, - WindowStartupLocation = WindowStartupLocation.CenterOwner, - Width = 200, - Height = 200, - Content = new Border - { - Background = Brushes.Red, - CornerRadius = new CornerRadius(100), - } - }; - - window.PointerPressed += (_, _) => - { - window.Close(); - backgroundWindow.Close(); - }; - - backgroundWindow.Show(this); - window.Show(backgroundWindow); - } - - private void ShowTransparentPopup() + private void Pager_SelectionChanged(object? sender, SelectionChangedEventArgs e) { - var popup = new Popup - { - WindowManagerAddShadowHint = false, - Placement = PlacementMode.AnchorAndGravity, - PlacementAnchor = PopupAnchor.Top, - PlacementGravity = PopupGravity.Bottom, - Width= 200, - Height= 200, - Child = new Border - { - Background = Brushes.Red, - CornerRadius = new CornerRadius(100), - } - }; - - // Show a background window to make sure the color behind the transparent window is - // a known color (green). - var backgroundWindow = new Window - { - Title = "Transparent Popup Background", - Name = "TransparentPopupBackground", - Width = 200, - Height = 200, - Background = Brushes.Green, - WindowStartupLocation = WindowStartupLocation.CenterOwner, - Content = new Border - { - Name = "PopupContainer", - Child = popup, - [AutomationProperties.AccessibilityViewProperty] = AccessibilityView.Content, - } - }; - - backgroundWindow.PointerPressed += (_, _) => backgroundWindow.Close(); - backgroundWindow.Show(this); - - popup.Open(); + if (Pager.SelectedItem is Page page) + PagerContent.Child = page.CreateContent(); } - private void SendToBack() + private void OnPositionChanged(object? sender, PixelPointEventArgs e) { - var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; - - foreach (var window in lifetime.Windows.ToArray()) + // HACK: Toggling the window decorations can cause the window to be moved off screen, + // causing test failures. Until this bug is fixed, detect this and move the window + // to the screen origin. See #11411. + if (Screens.ScreenFromWindow(this) is { } screen) { - window.Activate(); - } - } - - private void RestoreAll() - { - var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; + var bounds = new PixelRect( + e.Point, + PixelSize.FromSize(ClientSize, DesktopScaling)); - foreach (var window in lifetime.Windows.ToArray()) - { - window.Show(); - if (window.WindowState == WindowState.Minimized) - window.WindowState = WindowState.Normal; + if (!screen.WorkingArea.Contains(bounds)) + Position = screen.WorkingArea.Position; } } - private void InitializeGesturesTab() - { - var gestureBorder = this.GetControl("GestureBorder"); - var gestureBorder2 = this.GetControl("GestureBorder2"); - var lastGesture = this.GetControl("LastGesture"); - var resetGestures = this.GetControl + + + + + + diff --git a/samples/IntegrationTestApp/Pages/ButtonPage.axaml.cs b/samples/IntegrationTestApp/Pages/ButtonPage.axaml.cs new file mode 100644 index 0000000000..540ce839a3 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/ButtonPage.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace IntegrationTestApp.Pages; + +public partial class ButtonPage : UserControl +{ + public ButtonPage() + { + InitializeComponent(); + } +} diff --git a/samples/IntegrationTestApp/Pages/CheckBoxPage.axaml b/samples/IntegrationTestApp/Pages/CheckBoxPage.axaml new file mode 100644 index 0000000000..cdb61b53a4 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/CheckBoxPage.axaml @@ -0,0 +1,12 @@ + + + Unchecked + Checked + ThreeState + + diff --git a/samples/IntegrationTestApp/Pages/CheckBoxPage.axaml.cs b/samples/IntegrationTestApp/Pages/CheckBoxPage.axaml.cs new file mode 100644 index 0000000000..6863f62387 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/CheckBoxPage.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace IntegrationTestApp.Pages; + +public partial class CheckBoxPage : UserControl +{ + public CheckBoxPage() + { + InitializeComponent(); + } +} diff --git a/samples/IntegrationTestApp/Pages/ComboBoxPage.axaml b/samples/IntegrationTestApp/Pages/ComboBoxPage.axaml new file mode 100644 index 0000000000..6068b06e85 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/ComboBoxPage.axaml @@ -0,0 +1,16 @@ + + + + Item 0 + Item 1 + + Wrap Selection + + + + diff --git a/samples/IntegrationTestApp/Pages/ComboBoxPage.axaml.cs b/samples/IntegrationTestApp/Pages/ComboBoxPage.axaml.cs new file mode 100644 index 0000000000..eb9b66de76 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/ComboBoxPage.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace IntegrationTestApp.Pages; + +public partial class ComboBoxPage : UserControl +{ + public ComboBoxPage() + { + InitializeComponent(); + } + + private void ComboBoxSelectionClear_Click(object? sender, RoutedEventArgs e) + { + BasicComboBox.SelectedIndex = -1; + } + + private void ComboBoxSelectFirst_Click(object? sender, RoutedEventArgs e) + { + BasicComboBox.SelectedIndex = 0; + } +} diff --git a/samples/IntegrationTestApp/Pages/ContextMenuPage.axaml b/samples/IntegrationTestApp/Pages/ContextMenuPage.axaml new file mode 100644 index 0000000000..7d494bb277 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/ContextMenuPage.axaml @@ -0,0 +1,17 @@ + + + + + diff --git a/samples/IntegrationTestApp/Pages/ContextMenuPage.axaml.cs b/samples/IntegrationTestApp/Pages/ContextMenuPage.axaml.cs new file mode 100644 index 0000000000..e42d9f232e --- /dev/null +++ b/samples/IntegrationTestApp/Pages/ContextMenuPage.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace IntegrationTestApp.Pages; + +public partial class ContextMenuPage : UserControl +{ + public ContextMenuPage() + { + InitializeComponent(); + } +} diff --git a/samples/IntegrationTestApp/Pages/DesktopPage.axaml b/samples/IntegrationTestApp/Pages/DesktopPage.axaml new file mode 100644 index 0000000000..a5495bd347 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/DesktopPage.axaml @@ -0,0 +1,14 @@ + + + Tray Icon Clicked + Tray Icon Menu Clicked + + + + + + + + + diff --git a/samples/IntegrationTestApp/Pages/GesturesPage.axaml.cs b/samples/IntegrationTestApp/Pages/GesturesPage.axaml.cs new file mode 100644 index 0000000000..907edb973c --- /dev/null +++ b/samples/IntegrationTestApp/Pages/GesturesPage.axaml.cs @@ -0,0 +1,44 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace IntegrationTestApp.Pages; + +public partial class GesturesPage : UserControl +{ + public GesturesPage() + { + InitializeComponent(); + } + + private void GestureBorder_Tapped(object? sender, TappedEventArgs e) + { + LastGesture.Text = "Tapped"; + } + + private void GestureBorder_DoubleTapped(object? sender, TappedEventArgs e) + { + LastGesture.Text = "DoubleTapped"; + + // Testing #8733 + GestureBorder.IsVisible = false; + GestureBorder2.IsVisible = true; + } + + private void GestureBorder_RightTapped(object? sender, RoutedEventArgs e) + { + LastGesture.Text = "RightTapped"; + } + + private void GestureBorder2_DoubleTapped(object? sender, TappedEventArgs e) + { + LastGesture.Text = "DoubleTapped2"; + } + + private void ResetGestures_Click(object? sender, RoutedEventArgs e) + { + LastGesture.Text = string.Empty; + GestureBorder.IsVisible = true; + GestureBorder2.IsVisible = false; + } +} diff --git a/samples/IntegrationTestApp/Pages/ListBoxPage.axaml b/samples/IntegrationTestApp/Pages/ListBoxPage.axaml new file mode 100644 index 0000000000..4e23cd8e37 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/ListBoxPage.axaml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/samples/IntegrationTestApp/Pages/ListBoxPage.axaml.cs b/samples/IntegrationTestApp/Pages/ListBoxPage.axaml.cs new file mode 100644 index 0000000000..3bff7d9231 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/ListBoxPage.axaml.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace IntegrationTestApp.Pages; + +public partial class ListBoxPage : UserControl +{ + public ListBoxPage() + { + InitializeComponent(); + ListBoxItems = Enumerable.Range(0, 100).Select(x => "Item " + x).ToList(); + DataContext = this; + } + + public List ListBoxItems { get; } + + private void ListBoxSelectionClear_Click(object? sender, RoutedEventArgs e) + { + BasicListBox.SelectedIndex = -1; + } +} diff --git a/samples/IntegrationTestApp/Pages/MenuPage.axaml b/samples/IntegrationTestApp/Pages/MenuPage.axaml new file mode 100644 index 0000000000..36c2636f3d --- /dev/null +++ b/samples/IntegrationTestApp/Pages/MenuPage.axaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + None + + + + + diff --git a/samples/IntegrationTestApp/Pages/MenuPage.axaml.cs b/samples/IntegrationTestApp/Pages/MenuPage.axaml.cs new file mode 100644 index 0000000000..8be695f8ab --- /dev/null +++ b/samples/IntegrationTestApp/Pages/MenuPage.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace IntegrationTestApp.Pages; + +public partial class MenuPage : UserControl +{ + public MenuPage() + { + InitializeComponent(); + } + + private void MenuClicked(object? sender, RoutedEventArgs e) + { + var clickedMenuItemTextBlock = ClickedMenuItem; + clickedMenuItemTextBlock.Text = (sender as MenuItem)?.Header?.ToString(); + } + + + private void MenuClickedMenuItemReset_Click(object? sender, RoutedEventArgs e) + { + ClickedMenuItem.Text = "None"; + } +} diff --git a/samples/IntegrationTestApp/Pages/PointerPage.axaml b/samples/IntegrationTestApp/Pages/PointerPage.axaml new file mode 100644 index 0000000000..ba6016c9b5 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/PointerPage.axaml @@ -0,0 +1,19 @@ + + + + + Show Dialog + + + + diff --git a/samples/IntegrationTestApp/Pages/PointerPage.axaml.cs b/samples/IntegrationTestApp/Pages/PointerPage.axaml.cs new file mode 100644 index 0000000000..b34798be10 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/PointerPage.axaml.cs @@ -0,0 +1,47 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; + +namespace IntegrationTestApp.Pages; + +public partial class PointerPage : UserControl +{ + public PointerPage() + { + InitializeComponent(); + } + + private void PointerPageShowDialog_PointerPressed(object? sender, PointerPressedEventArgs e) + { + void CaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + PointerCaptureStatus.Text = "None"; + ((Control)sender!).PointerCaptureLost -= CaptureLost; + } + + var window = TopLevel.GetTopLevel(this) as Window ?? + throw new AvaloniaInternalException("PointerPage is not attached to a Window."); + var captured = e.Pointer.Captured as Control; + + if (captured is not null) + { + captured.PointerCaptureLost += CaptureLost; + } + + PointerCaptureStatus.Text = captured?.ToString() ?? "None"; + + var dialog = new Window + { + Width = 200, + Height = 200, + }; + + dialog.Content = new Button + { + Content = "Close", + Command = new DelegateCommand(() => dialog.Close()), + }; + + dialog.ShowDialog(window); + } +} diff --git a/samples/IntegrationTestApp/Pages/RadioButtonPage.axaml b/samples/IntegrationTestApp/Pages/RadioButtonPage.axaml new file mode 100644 index 0000000000..f66b9b9f7b --- /dev/null +++ b/samples/IntegrationTestApp/Pages/RadioButtonPage.axaml @@ -0,0 +1,14 @@ + + + Sample RadioButton + + Three States: Option 1 + Three States: Option 2 + + + diff --git a/samples/IntegrationTestApp/Pages/RadioButtonPage.axaml.cs b/samples/IntegrationTestApp/Pages/RadioButtonPage.axaml.cs new file mode 100644 index 0000000000..115ff6f2f0 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/RadioButtonPage.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace IntegrationTestApp.Pages; + +public partial class RadioButtonPage : UserControl +{ + public RadioButtonPage() + { + InitializeComponent(); + } +} diff --git a/samples/IntegrationTestApp/Pages/ScreensPage.axaml b/samples/IntegrationTestApp/Pages/ScreensPage.axaml new file mode 100644 index 0000000000..2d95c4719a --- /dev/null +++ b/samples/IntegrationTestApp/Pages/ScreensPage.axaml @@ -0,0 +1,19 @@ + + + + + diff --git a/samples/IntegrationTestApp/Pages/SliderPage.axaml.cs b/samples/IntegrationTestApp/Pages/SliderPage.axaml.cs new file mode 100644 index 0000000000..72f0174fed --- /dev/null +++ b/samples/IntegrationTestApp/Pages/SliderPage.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace IntegrationTestApp.Pages; + +public partial class SliderPage : UserControl +{ + public SliderPage() + { + InitializeComponent(); + } + + private void ResetSliders_Click(object? sender, RoutedEventArgs e) + { + HorizontalSlider.Value = 50; + } +} diff --git a/samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml b/samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml new file mode 100644 index 0000000000..21a5b1d883 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/IntegrationTestApp/Pages/WindowPage.axaml.cs b/samples/IntegrationTestApp/Pages/WindowPage.axaml.cs new file mode 100644 index 0000000000..5549e537d3 --- /dev/null +++ b/samples/IntegrationTestApp/Pages/WindowPage.axaml.cs @@ -0,0 +1,199 @@ +using System.Linq; +using Avalonia; +using Avalonia.Automation; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace IntegrationTestApp.Pages; + +public partial class WindowPage : UserControl +{ + public WindowPage() + { + InitializeComponent(); + } + + private Window Window => TopLevel.GetTopLevel(this) as Window ?? + throw new AvaloniaInternalException("WindowPage is not attached to a Window."); + + private void ShowWindow_Click(object? sender, RoutedEventArgs e) + { + var size = !string.IsNullOrWhiteSpace(ShowWindowSize.Text) ? Size.Parse(ShowWindowSize.Text) : (Size?)null; + var window = new ShowWindowTest + { + WindowStartupLocation = (WindowStartupLocation)ShowWindowLocation.SelectedIndex, + CanResize = ShowWindowCanResize.IsChecked ?? false, + }; + + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + // Make sure the windows have unique names and AutomationIds. + var existing = lifetime.Windows.OfType().Count(); + if (existing > 0) + { + AutomationProperties.SetAutomationId(window, window.Name + (existing + 1)); + window.Title += $" {existing + 1}"; + } + } + + if (size.HasValue) + { + window.Width = size.Value.Width; + window.Height = size.Value.Height; + } + + ShowWindowSize.Text = string.Empty; + window.ExtendClientAreaToDecorationsHint = ShowWindowExtendClientAreaToDecorationsHint.IsChecked ?? false; + window.SystemDecorations = (SystemDecorations)ShowWindowSystemDecorations.SelectedIndex; + window.WindowState = (WindowState)ShowWindowState.SelectedIndex; + + switch (ShowWindowMode.SelectedIndex) + { + case 0: + window.Show(); + break; + case 1: + window.Show(Window); + break; + case 2: + window.ShowDialog(Window); + break; + } + } + + private void ShowTransparentWindow_Click(object? sender, RoutedEventArgs e) + { + // Show a background window to make sure the color behind the transparent window is + // a known color (green). + var backgroundWindow = new Window + { + Title = "Transparent Window Background", + Name = "TransparentWindowBackground", + Width = 300, + Height = 300, + Background = Brushes.Green, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + + // This is the transparent window with a red circle. + var window = new Window + { + Title = "Transparent Window", + Name = "TransparentWindow", + SystemDecorations = SystemDecorations.None, + Background = Brushes.Transparent, + TransparencyLevelHint = new[] { WindowTransparencyLevel.Transparent }, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Width = 200, + Height = 200, + Content = new Border + { + Background = Brushes.Red, + CornerRadius = new CornerRadius(100), + } + }; + + window.PointerPressed += (_, _) => + { + window.Close(); + backgroundWindow.Close(); + }; + + backgroundWindow.Show(Window); + window.Show(backgroundWindow); + } + + private void ShowTransparentPopup_Click(object? sender, RoutedEventArgs e) + { + var popup = new Popup + { + WindowManagerAddShadowHint = false, + Placement = PlacementMode.AnchorAndGravity, + PlacementAnchor = PopupAnchor.Top, + PlacementGravity = PopupGravity.Bottom, + Width = 200, + Height = 200, + Child = new Border + { + Background = Brushes.Red, + CornerRadius = new CornerRadius(100), + } + }; + + // Show a background window to make sure the color behind the transparent window is + // a known color (green). + var backgroundWindow = new Window + { + Title = "Transparent Popup Background", + Name = "TransparentPopupBackground", + Width = 200, + Height = 200, + Background = Brushes.Green, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Content = new Border + { + Name = "PopupContainer", + Child = popup, + [AutomationProperties.AccessibilityViewProperty] = AccessibilityView.Content, + } + }; + + backgroundWindow.PointerPressed += (_, _) => backgroundWindow.Close(); + backgroundWindow.Show(Window); + + popup.Open(); + } + + private void SendToBack_Click(object? sender, RoutedEventArgs e) + { + var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; + + foreach (var window in lifetime.Windows.ToArray()) + { + window.Activate(); + } + } + + private void EnterFullscreen_Click(object? sender, RoutedEventArgs e) + { + Window.WindowState = WindowState.FullScreen; + } + + private void ExitFullscreen_Click(object? sender, RoutedEventArgs e) + { + Window.WindowState = WindowState.Normal; + } + + private void RestoreAll_Click(object? sender, RoutedEventArgs e) + { + var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; + + foreach (var window in lifetime.Windows.ToArray()) + { + window.Show(); + if (window.WindowState == WindowState.Minimized) + window.WindowState = WindowState.Normal; + } + } + + private void ShowTopmostWindow_Click(object? sender, RoutedEventArgs e) + { + var mainWindow = new TopmostWindowTest("OwnerWindow") + { + Topmost = true, + Title = "Owner Window" + }; + var ownedWindow = new TopmostWindowTest("OwnedWindow") + { + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Title = "Owned Window" + }; + + mainWindow.Show(); + ownedWindow.Show(mainWindow); + } +} diff --git a/samples/IntegrationTestApp/Program.cs b/samples/IntegrationTestApp/Program.cs index 6603450b85..43c936bb1c 100644 --- a/samples/IntegrationTestApp/Program.cs +++ b/samples/IntegrationTestApp/Program.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Avalonia; +using IntegrationTestApp.Embedding; namespace IntegrationTestApp { @@ -31,6 +32,13 @@ namespace IntegrationTestApp public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() + .AfterSetup(builder => + { + NativeTextBox.Factory = + OperatingSystem.IsWindows() ? new Win32TextBoxFactory() : + OperatingSystem.IsMacOS() ? new MacOSTextBoxFactory() : + null; + }) .LogToTrace(); } } diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 720ff6c344..9e194f0e7d 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -5,7 +5,7 @@ Name="SecondaryWindow" x:DataType="Window" Title="Show Window Test"> - + this.GetControl("CurrentPosition").Text = $"{Position}"; + PositionChanged += (s, e) => CurrentPosition.Text = $"{Position}"; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - _orderTextBox = this.GetControl("CurrentOrder"); + _orderTextBox = CurrentOrder; _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; _timer.Tick += TimerOnTick; _timer.Start(); } } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } protected override void OnOpened(EventArgs e) { base.OnOpened(e); var scaling = PlatformImpl!.DesktopScaling; - this.GetControl("CurrentPosition").Text = $"{Position}"; - this.GetControl("CurrentScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; - this.GetControl("CurrentScaling").Text = $"{scaling}"; + CurrentPosition.Text = $"{Position}"; + CurrentScreenRect.Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; + CurrentScaling.Text = $"{scaling}"; if (Owner is not null) { - var ownerRect = this.GetControl("CurrentOwnerRect"); var owner = (Window)Owner; - ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}"; + CurrentOwnerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}"; } } diff --git a/samples/IntegrationTestApp/TopmostWindowTest.axaml b/samples/IntegrationTestApp/TopmostWindowTest.axaml new file mode 100644 index 0000000000..fbd2d9cf40 --- /dev/null +++ b/samples/IntegrationTestApp/TopmostWindowTest.axaml @@ -0,0 +1,17 @@ + + + + + + diff --git a/samples/IntegrationTestApp/TopmostWindowTest.axaml.cs b/samples/IntegrationTestApp/TopmostWindowTest.axaml.cs new file mode 100644 index 0000000000..c3ee80e3d6 --- /dev/null +++ b/samples/IntegrationTestApp/TopmostWindowTest.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; + +namespace IntegrationTestApp; + +public partial class TopmostWindowTest : Window +{ + public TopmostWindowTest(string name) + { + Name = name; + InitializeComponent(); + PositionChanged += (s, e) => CurrentPosition.Text = $"{Position}"; + } + + private void Button_OnClick(object? sender, RoutedEventArgs e) + { + Position += new PixelPoint(100, 100); + } +} diff --git a/samples/IntegrationTestApp/ViewModels/MainWindowViewModel.cs b/samples/IntegrationTestApp/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..86eb13ec5a --- /dev/null +++ b/samples/IntegrationTestApp/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using IntegrationTestApp.Models; + +namespace IntegrationTestApp.ViewModels; + +internal class MainWindowViewModel : ViewModelBase +{ + private Page? _selectedPage; + + public MainWindowViewModel(IEnumerable pages) + { + Pages = new(pages); + } + + public ObservableCollection Pages { get; } + + public Page? SelectedPage + { + get => _selectedPage; + set => RaiseAndSetIfChanged(ref _selectedPage, value); + } +} diff --git a/samples/IntegrationTestApp/ViewModels/ViewModelBase.cs b/samples/IntegrationTestApp/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000000..521382b863 --- /dev/null +++ b/samples/IntegrationTestApp/ViewModels/ViewModelBase.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace IntegrationTestApp.ViewModels; + +internal class ViewModelBase : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + RaisePropertyChanged(propertyName); + return true; + } + return false; + } + + protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); +} diff --git a/samples/IntegrationTestApp/app.manifest b/samples/IntegrationTestApp/app.manifest new file mode 100644 index 0000000000..db90057191 --- /dev/null +++ b/samples/IntegrationTestApp/app.manifest @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/AssemblyLoadContextH.cs b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/AssemblyLoadContextH.cs index 780be4d4b8..607ec49b12 100644 --- a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/AssemblyLoadContextH.cs +++ b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/AssemblyLoadContextH.cs @@ -22,13 +22,16 @@ public class AssemblyLoadContextH : AssemblyLoadContext Unloading += (sender) => { AvaloniaPropertyRegistry.Instance.UnregisterByModule(sender.Assemblies.First().DefinedTypes); - Application.Current.Styles.Remove(MainWindow.Style); - AssetLoader.InvalidateAssemblyCache(sender.Assemblies.First().GetName().Name); - MainWindow.Style= null; + + if (MainWindow.Style is { } style) + Application.Current?.Styles.Remove(style); + + AssetLoader.InvalidateAssemblyCache(sender.Assemblies.First().GetName().Name!); + MainWindow.Style = null; }; } - protected override Assembly Load(AssemblyName assemblyName) + protected override Assembly? Load(AssemblyName assemblyName) { var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) diff --git a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/MainWindow.axaml.cs b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/MainWindow.axaml.cs index 959740da59..aee3a60a01 100644 --- a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/MainWindow.axaml.cs +++ b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/MainWindow.axaml.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Threading; @@ -33,11 +34,11 @@ public partial class MainWindow : Window this.AttachDevTools(); } } - private PlugTool _plugTool; + private PlugTool? _plugTool; protected override void OnOpened(EventArgs e) { base.OnOpened(e); - test(); + Test(); //Content = _plugTool.FindControl("UnloadableAssemblyLoadContextPlug.TestControl"); @@ -75,7 +76,7 @@ public partial class MainWindow : Window Thread.CurrentThread.IsBackground = false; - var weakReference = _plugTool.Unload(); + var weakReference = _plugTool!.Unload(); while (weakReference.IsAlive) { GC.Collect(); @@ -88,8 +89,9 @@ public partial class MainWindow : Window } - public static IStyle Style; - public void test(){ + public static IStyle? Style; + + public void Test() { //Notice : 你可以删除UnloadableAssemblyLoadContextPlug.dll所在文件夹中有关Avalonia的所有Dll,但这不是必须的 //Notice : You can delete all Dlls about Avalonia in the folder where UnloadableAssemblyLoadContextPlug.dll is located, but this is not necessary @@ -97,7 +99,6 @@ public partial class MainWindow : Window var AssemblyLoadContextH = new AssemblyLoadContextH(fileInfo.FullName,"test"); var assembly = AssemblyLoadContextH.LoadFromAssemblyPath(fileInfo.FullName); - var assemblyDescriptorResolver = _plugTool=new PlugTool(); _plugTool.AssemblyLoadContextH = AssemblyLoadContextH; @@ -106,13 +107,13 @@ public partial class MainWindow : Window styleInclude.Source=new Uri("ControlStyle.axaml", UriKind.Relative); styles.Add(styleInclude); Style = styles; - Application.Current.Styles.Add(styles); + Application.Current!.Styles.Add(styles); foreach (var type in assembly.GetTypes()) { if (type.FullName=="AvaloniaPlug.Window1") { //创建type实例 - Window instance = (Window)type.GetConstructor( new Type[0]).Invoke(null); + Window? instance = (Window)type.GetConstructor([])!.Invoke(null); Dispatcher.UIThread.InvokeAsync(() => { diff --git a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/PlugTool.cs b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/PlugTool.cs index fb5cc28265..2b240e3a91 100644 --- a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/PlugTool.cs +++ b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/PlugTool.cs @@ -6,23 +6,23 @@ namespace UnloadableAssemblyLoadContext; public class PlugTool { - public AssemblyLoadContextH AssemblyLoadContextH; + public AssemblyLoadContextH? AssemblyLoadContextH; public WeakReference Unload() { var weakReference = new WeakReference(AssemblyLoadContextH); - AssemblyLoadContextH.Unload(); + AssemblyLoadContextH?.Unload(); AssemblyLoadContextH = null; return weakReference; } public Control? FindControl(string type) { - var type1 = AssemblyLoadContextH.Assemblies. + var type1 = AssemblyLoadContextH!.Assemblies. FirstOrDefault(x => x.GetName().Name == "UnloadableAssemblyLoadContextPlug")?. GetType(type); - if (type1.IsSubclassOf(typeof(Control))) + if (type1 is not null && type1.IsSubclassOf(typeof(Control))) { - var constructorInfo = type1.GetConstructor( Type.EmptyTypes).Invoke(null) as Control; + var constructorInfo = type1.GetConstructor([])!.Invoke(null) as Control; return constructorInfo; } diff --git a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext.csproj b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext.csproj index e39a35a294..9ad92d3e62 100644 --- a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext.csproj +++ b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContext.csproj @@ -10,19 +10,10 @@ - - %(Filename) - - - Designer - - - - - - - + + + diff --git a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContextPlug/Program.cs b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContextPlug/Program.cs deleted file mode 100644 index 60c37ec32f..0000000000 --- a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContextPlug/Program.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace AvaloniaPlug; - -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. - private static string test = "23"; -} \ No newline at end of file diff --git a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContextPlug/Window1.axaml b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContextPlug/Window1.axaml index 83a389f31e..b9724f2c51 100644 --- a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContextPlug/Window1.axaml +++ b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContextPlug/Window1.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:avaloniaPlug="clr-namespace:AvaloniaPlug" xmlns:unloadableAssemblyLoadContextPlug="clr-namespace:UnloadableAssemblyLoadContextPlug" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="UnloadableAssemblyLoadContextPlug.Window1" diff --git a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContextPlug/Window1.axaml.cs b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContextPlug/Window1.axaml.cs index 8236d1a338..e4f9c619cd 100644 --- a/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContextPlug/Window1.axaml.cs +++ b/samples/UnloadableAssemblyLoadContext/UnloadableAssemblyLoadContextPlug/Window1.axaml.cs @@ -1,8 +1,5 @@ -using System.Diagnostics; -using Avalonia; -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Markup.Xaml; -using AvaloniaPlug; namespace UnloadableAssemblyLoadContextPlug; diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index bf9237deec..0d2ae2c1ca 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Android; using Avalonia.Android.Platform; using Avalonia.Android.Platform.Input; +using Avalonia.Android.Platform.Vulkan; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Input.Platform; @@ -11,6 +12,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; +using Avalonia.Vulkan; namespace Avalonia { @@ -38,7 +40,12 @@ namespace Avalonia /// /// Enables android EGL rendering. /// - Egl = 2 + Egl = 2, + + /// + /// Enables Vulkan rendering + /// + Vulkan = 3 } public sealed class AndroidPlatformOptions @@ -81,6 +88,7 @@ namespace Avalonia.Android .Bind().ToSingleton() .Bind().ToConstant(new ChoreographerTimer()) .Bind().ToSingleton() + .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { })) .Bind().ToConstant(new AndroidActivatableLifetime()); var graphics = InitializeGraphics(Options); @@ -114,6 +122,13 @@ namespace Avalonia.Android return egl; } } + + if (renderingMode == AndroidRenderingMode.Vulkan) + { + var vulkan = VulkanSupport.TryInitialize(AvaloniaLocator.Current.GetService() ?? new()); + if (vulkan != null) + return vulkan; + } } throw new InvalidOperationException($"{nameof(AndroidPlatformOptions)}.{nameof(AndroidPlatformOptions.RenderingMode)} has a value of \"{string.Join(", ", opts.RenderingMode)}\", but no options were applied."); diff --git a/src/Android/Avalonia.Android/AndroidViewControlHandle.cs b/src/Android/Avalonia.Android/AndroidViewControlHandle.cs index 6d14ea787f..b2ccc6ff6e 100644 --- a/src/Android/Avalonia.Android/AndroidViewControlHandle.cs +++ b/src/Android/Avalonia.Android/AndroidViewControlHandle.cs @@ -1,5 +1,5 @@ using System; - +using Android.Runtime; using Android.Views; using Avalonia.Controls.Platform; @@ -7,20 +7,16 @@ using Avalonia.Platform; namespace Avalonia.Android { - public class AndroidViewControlHandle : INativeControlHostDestroyableControlHandle + public class AndroidViewControlHandle : PlatformHandle, INativeControlHostDestroyableControlHandle { - internal const string AndroidDescriptor = "JavaObjectHandle"; + internal static string AndroidViewDescriptor = "android.view.View"; - public AndroidViewControlHandle(View view) + public AndroidViewControlHandle(View view) : base(view.Handle, AndroidViewDescriptor) { View = view; } - public View View { get; } - - public string HandleDescriptor => AndroidDescriptor; - - IntPtr IPlatformHandle.Handle => View.Handle; + public View View { get; private set; } public void Destroy() { diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index b0b0712480..170cc088fb 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -17,4 +17,8 @@ + + + + diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs index 29a3949c70..2e61149fd7 100644 --- a/src/Android/Avalonia.Android/AvaloniaActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs @@ -9,6 +9,8 @@ using Android.OS; using Android.Runtime; using Android.Views; using AndroidX.AppCompat.App; +using Avalonia.Platform; +using Avalonia.Android.Platform; using Avalonia.Android.Platform.Storage; using Avalonia.Controls.ApplicationLifetimes; @@ -23,6 +25,7 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity private EventHandler? _onActivated, _onDeactivated; private GlobalLayoutListener? _listener; private object? _content; + private bool _contentViewSet; internal AvaloniaView? _view; public Action? ActivityResult { get; set; } @@ -41,6 +44,17 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity _content = value; if (_view is not null) { + if (!_contentViewSet) + { + _contentViewSet = true; + + SetContentView(_view); + + _listener = new GlobalLayoutListener(_view); + + _view.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener); + } + _view.Content = _content; } } @@ -78,14 +92,13 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity base.OnCreate(savedInstanceState); - SetContentView(_view); - - _listener = new GlobalLayoutListener(_view); - - _view.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener); + if (Avalonia.Application.Current?.TryGetFeature() + is AndroidActivatableLifetime activatableLifetime) + { + activatableLifetime.CurrentIntendActivity = this; + } - // TODO: we probably don't need to create AvaloniaView, if it's just a protocol activation, and main activity is already created. - if (Intent?.Data is {} androidUri + if (Intent?.Data is { } androidUri && androidUri.IsAbsolute && Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var uri)) { @@ -124,21 +137,27 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity { attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges; } + + // We inform the ContentView that it has become visible. OnVisibleChanged() sometimes doesn't get called. Issue #15807. + _view?.OnVisibilityChanged(true); } protected override void OnDestroy() { if (_view is not null) { + if (_listener is not null) + { + _view.ViewTreeObserver?.RemoveOnGlobalLayoutListener(_listener); + } _view.Content = null; - _view.ViewTreeObserver?.RemoveOnGlobalLayoutListener(_listener); _view.Dispose(); _view = null; } base.OnDestroy(); } - + protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent? data) { base.OnActivityResult(requestCode, resultCode, data); diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index f010899105..bf3a7b742a 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -10,18 +10,6 @@ public class AvaloniaMainActivity : AvaloniaActivity { private protected static SingleViewLifetime? Lifetime; - public override void OnCreate(Bundle? savedInstanceState, PersistableBundle? persistentState) - { - // Global IActivatableLifetime expects a main activity, so we need to replace it on each OnCreate. - if (Avalonia.Application.Current?.TryGetFeature() - is AndroidActivatableLifetime activatableLifetime) - { - activatableLifetime.Activity = this; - } - - base.OnCreate(savedInstanceState, persistentState); - } - private protected override void InitializeAvaloniaView(object? initialContent) { // Android can run OnCreate + InitializeAvaloniaView multiple times per process lifetime. @@ -31,8 +19,10 @@ public class AvaloniaMainActivity : AvaloniaActivity // We need this AfterSetup callback to match iOS/Browser behavior and ensure that view/toplevel is available in custom AfterSetup calls. if (Lifetime is not null) { - Lifetime.Activity = this; + initialContent ??= Lifetime.MainView; + _view = new AvaloniaView(this) { Content = initialContent }; + Lifetime.Activity = this; } else { @@ -53,6 +43,12 @@ public class AvaloniaMainActivity : AvaloniaActivity if (_view is null) throw new InvalidOperationException("Unknown error: AvaloniaView initialization has failed."); } + + if (Avalonia.Application.Current?.TryGetFeature() + is AndroidActivatableLifetime activatableLifetime) + { + activatableLifetime.CurrentMainActivity = this; + } } protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure().UseAndroid(); diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index f72da82010..cb03d85fc2 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -21,6 +21,7 @@ namespace Avalonia.Android private readonly ViewImpl _view; private IDisposable? _timerSubscription; + private bool _surfaceCreated; public AvaloniaView(Context context) : base(context) { @@ -32,6 +33,18 @@ namespace Avalonia.Android this.SetBackgroundColor(global::Android.Graphics.Color.Transparent); OnConfigurationChanged(); + + _view.InternalView.SurfaceWindowCreated += InternalView_SurfaceWindowCreated; + } + + private void InternalView_SurfaceWindowCreated(object? sender, EventArgs e) + { + _surfaceCreated = true; + + if (Visibility == ViewStates.Visible) + { + OnVisibilityChanged(true); + } } internal TopLevelImpl TopLevelImpl => _view; @@ -43,9 +56,10 @@ namespace Avalonia.Android set { _root.Content = value; } } - protected override void Dispose(bool disposing) + internal new void Dispose() { - base.Dispose(disposing); + OnVisibilityChanged(false); + _surfaceCreated = false; _root?.Dispose(); _root = null!; } @@ -68,9 +82,11 @@ namespace Avalonia.Android OnVisibilityChanged(visibility == ViewStates.Visible); } - private void OnVisibilityChanged(bool isVisible) + internal void OnVisibilityChanged(bool isVisible) { - if (isVisible) + if (_root == null || !_surfaceCreated) + return; + if (isVisible && _timerSubscription == null) { if (AvaloniaLocator.Current.GetService() is ChoreographerTimer timer) { @@ -84,10 +100,11 @@ namespace Avalonia.Android (insetsManager as AndroidInsetsManager)?.ApplyStatusBarState(); } } - else + else if (!isVisible && _timerSubscription != null) { _root.StopRendering(); _timerSubscription?.Dispose(); + _timerSubscription = null; } } @@ -104,6 +121,7 @@ namespace Avalonia.Android var settings = AvaloniaLocator.Current.GetRequiredService() as AndroidPlatformSettings; settings?.OnViewConfigurationChanged(context); + ((AndroidScreens)_view.TryGetFeature()!).OnChanged(); } } diff --git a/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs b/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs index a09c0ea88a..9679d39d5e 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidActivatableLifetime.cs @@ -5,32 +5,71 @@ namespace Avalonia.Android.Platform; internal class AndroidActivatableLifetime : ActivatableLifetimeBase { - private IAvaloniaActivity? _activity; + private IAvaloniaActivity? _mainActivity, _intendActivity; - public IAvaloniaActivity? Activity + /// + /// While we primarily handle main activity lifecycle events. + /// Any secondary activity might send protocol or file activation. + /// + public IAvaloniaActivity? CurrentIntendActivity { - get => _activity; + get => _intendActivity; set { - if (_activity is not null) + if (_intendActivity is not null) { - _activity.Activated -= ActivityOnActivated; - _activity.Deactivated -= ActivityOnDeactivated; + _intendActivity.Activated -= IntendActivityOnActivated; } - _activity = value; + _intendActivity = value; - if (_activity is not null) + if (_intendActivity is not null) { - _activity.Activated += ActivityOnActivated; - _activity.Deactivated += ActivityOnDeactivated; + _intendActivity.Activated += IntendActivityOnActivated; + } + } + } + + public IAvaloniaActivity? CurrentMainActivity + { + get => _mainActivity; + set + { + if (_mainActivity is not null) + { + _mainActivity.Activated -= MainActivityOnActivated; + _mainActivity.Deactivated -= MainActivityOnDeactivated; + } + + _mainActivity = value; + + if (_mainActivity is not null) + { + _mainActivity.Activated += MainActivityOnActivated; + _mainActivity.Deactivated += MainActivityOnDeactivated; } } } - public override bool TryEnterBackground() => (_activity as Activity)?.MoveTaskToBack(true) == true; + public override bool TryEnterBackground() => (_mainActivity as Activity)?.MoveTaskToBack(true) == true; - private void ActivityOnDeactivated(object? sender, ActivatedEventArgs e) => OnDeactivated(e); + private void MainActivityOnDeactivated(object? sender, ActivatedEventArgs e) => OnDeactivated(e); + + private void MainActivityOnActivated(object? sender, ActivatedEventArgs e) + { + if (!IsIntendActivation(e.Kind)) + { + OnActivated(e); + } + } + + private void IntendActivityOnActivated(object? sender, ActivatedEventArgs e) + { + if (IsIntendActivation(e.Kind)) + { + OnActivated(e); + } + } - private void ActivityOnActivated(object? sender, ActivatedEventArgs e) => OnActivated(e); + private static bool IsIntendActivation(ActivationKind kind) => kind is ActivationKind.File or ActivationKind.OpenUri; } diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index 18cad0aebf..992d37ed18 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -4,6 +4,7 @@ using Android.App; using Android.OS; using Android.Views; using Android.Views.Animations; +using AndroidX.Core.Graphics; using AndroidX.Core.View; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Animation.Easings; @@ -25,6 +26,7 @@ namespace Avalonia.Android.Platform private Color? _systemBarColor; private InputPaneState _state; private Rect _previousRect; + private Insets? _previousImeInset; private readonly bool _usesLegacyLayouts; private AndroidWindow Window => _activity.Window ?? throw new InvalidOperationException("Activity.Window must be set."); @@ -148,6 +150,19 @@ namespace Avalonia.Android.Platform State = insets.IsVisible(WindowInsetsCompat.Type.Ime()) ? InputPaneState.Open : InputPaneState.Closed; + // Workaround for weird inset values for android 11 + if(Build.VERSION.SdkInt == BuildVersionCodes.R) + { + var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); + if(_previousImeInset == default) + _previousImeInset = imeInset; + if(imeInset.Bottom != _previousImeInset.Bottom) + { + NotifyStateChanged(State, _previousRect, OccludedRect, TimeSpan.Zero, null); + } + _previousImeInset = imeInset; + } + return insets; } @@ -191,11 +206,6 @@ namespace Avalonia.Android.Platform { _statusBarTheme = value; - if (!_topLevel.View.IsShown) - { - return; - } - var compat = new WindowInsetsControllerCompat(Window, _topLevel.View); if (_isDefaultSystemBarLightTheme == null) @@ -229,11 +239,6 @@ namespace Avalonia.Android.Platform { _systemUiVisibility = value; - if (!_topLevel.View.IsShown) - { - return; - } - var compat = WindowCompat.GetInsetsController(Window, _topLevel.View); if (value == null || value.Value) diff --git a/src/Android/Avalonia.Android/Platform/AndroidLauncher.cs b/src/Android/Avalonia.Android/Platform/AndroidLauncher.cs index 1f1c2f8d2a..4aa793ee85 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidLauncher.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidLauncher.cs @@ -27,6 +27,7 @@ internal class AndroidLauncher : ILauncher var flags = ActivityFlags.ClearTop | ActivityFlags.NewTask; intent.SetFlags(flags); _context.StartActivity(intent); + return Task.FromResult(true); } } return Task.FromResult(false); @@ -49,6 +50,7 @@ internal class AndroidLauncher : ILauncher var flags = ActivityFlags.ClearTop | ActivityFlags.NewTask; chooserIntent.SetFlags(flags); _context.StartActivity(chooserIntent); + return Task.FromResult(true); } } return Task.FromResult(false); diff --git a/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs b/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs index 7cc1e70cfd..9edd207627 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs @@ -52,7 +52,7 @@ namespace Avalonia.Android.Platform }; } - public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == AndroidViewControlHandle.AndroidDescriptor; + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == AndroidViewControlHandle.AndroidViewDescriptor; private class AndroidNativeControlAttachment : INativeControlHostControlTopLevelAttachment { diff --git a/src/Android/Avalonia.Android/Platform/AndroidScreens.cs b/src/Android/Avalonia.Android/Platform/AndroidScreens.cs new file mode 100644 index 0000000000..667ec260fa --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/AndroidScreens.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using Android.Content; +using Android.Hardware.Display; +using Android.Runtime; +using Android.Util; +using Android.Views; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Platform; +using AndroidOrientation = global::Android.Content.Res.Orientation; + +namespace Avalonia.Android.Platform; + +internal class AndroidScreen(Display display) : PlatformScreen(new PlatformHandle(new IntPtr(display.DisplayId), "DisplayId")) +{ + public void Refresh(Context context) + { + DisplayName = display.Name; + + var naturalOrientation = ScreenOrientation.Portrait; + var rotation = display.Rotation; + + if (OperatingSystem.IsAndroidVersionAtLeast(30) + && display.DisplayId == context.Display?.DisplayId + && context.Resources?.DisplayMetrics is { } primaryMetrics) + { + IsPrimary = true; + Scaling = primaryMetrics.Density; + Bounds = WorkingArea = new(0, 0, primaryMetrics.WidthPixels, primaryMetrics.HeightPixels); + + var orientation = context.Resources.Configuration?.Orientation; + if (orientation == AndroidOrientation.Square) + naturalOrientation = ScreenOrientation.None; + else if (rotation is SurfaceOrientation.Rotation0 or SurfaceOrientation.Rotation180) + naturalOrientation = orientation == AndroidOrientation.Landscape ? + ScreenOrientation.Landscape : + ScreenOrientation.Portrait; + else + naturalOrientation = orientation == AndroidOrientation.Portrait ? + ScreenOrientation.Landscape : + ScreenOrientation.Portrait; + } + else + { + IsPrimary = false; + // These Display methods are deprecated since 31 SDK, + // But Android doesn't have any replacement, except for the primary screen. +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CA1422 // Validate platform compatibility +#pragma warning disable CA1416 // Validate platform compatibility + var displayMetrics = new DisplayMetrics(); + display.GetRealMetrics(displayMetrics); +#pragma warning restore CA1416 // Validate platform compatibility +#pragma warning restore CA1422 // Validate platform compatibility +#pragma warning restore CS0618 // Type or member is obsolete + Scaling = displayMetrics.Density; + Bounds = WorkingArea = new(0, 0, displayMetrics.WidthPixels, displayMetrics.HeightPixels); + } + + CurrentOrientation = (display.Rotation, naturalOrientation) switch + { + (_, ScreenOrientation.None) => ScreenOrientation.None, + (SurfaceOrientation.Rotation0, ScreenOrientation.Landscape) => ScreenOrientation.Landscape, + (SurfaceOrientation.Rotation90, ScreenOrientation.Landscape) => ScreenOrientation.Portrait, + (SurfaceOrientation.Rotation180, ScreenOrientation.Landscape) => ScreenOrientation.LandscapeFlipped, + (SurfaceOrientation.Rotation270, ScreenOrientation.Landscape) => ScreenOrientation.PortraitFlipped, + (SurfaceOrientation.Rotation0, _) => ScreenOrientation.Portrait, + (SurfaceOrientation.Rotation90, _) => ScreenOrientation.Landscape, + (SurfaceOrientation.Rotation180, _) => ScreenOrientation.PortraitFlipped, + (SurfaceOrientation.Rotation270, _) => ScreenOrientation.LandscapeFlipped, + _ => ScreenOrientation.Portrait + }; + } +} + +internal sealed class AndroidScreens : ScreensBase, IDisposable +{ + private readonly Context _context; + private readonly DisplayManager? _displayService; + private readonly DisplayListener? _listener; + + public AndroidScreens(Context context) : base(new DisplayComparer()) + { + _context = context; + _displayService = context.GetSystemService(Context.DisplayService).JavaCast(); + if (_displayService is not null) + { + _listener = new DisplayListener(this); + _displayService.RegisterDisplayListener(_listener, null); + } + } + + protected override IReadOnlyList GetAllScreenKeys() + { + if (_displayService?.GetDisplays() is { } displays) + { + return displays; + } + + if (OperatingSystem.IsAndroidVersionAtLeast(30) && _context.Display is { } defaultDisplay) + { + return [defaultDisplay]; + } + + return Array.Empty(); + } + + protected override AndroidScreen CreateScreenFromKey(Display display) => new(display); + + protected override void ScreenChanged(AndroidScreen screen) => screen.Refresh(_context); + + protected override Screen? ScreenFromTopLevelCore(ITopLevelImpl topLevel) + { + var display = ((TopLevelImpl)topLevel).View.Display; + return display is not null && TryGetScreen(display, out var screen) ? screen : null; + } + + protected override Screen? ScreenFromPointCore(PixelPoint point) => null; + protected override Screen? ScreenFromRectCore(PixelRect rect) => null; + + public void Dispose() + { + _displayService?.UnregisterDisplayListener(_listener); + _displayService?.Dispose(); + _listener?.Dispose(); + } + + private class DisplayListener(AndroidScreens screens) : Java.Lang.Object, DisplayManager.IDisplayListener + { + public void OnDisplayAdded(int displayId) => screens.OnChanged(); + public void OnDisplayChanged(int displayId) => screens.OnChanged(); + public void OnDisplayRemoved(int displayId) => screens.OnChanged(); + } + + private class DisplayComparer : IEqualityComparer + { + public bool Equals(Display? x, Display? y) => x?.DisplayId == y?.DisplayId; + public int GetHashCode(Display obj) => obj.DisplayId; + } +} diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs similarity index 57% rename from src/Android/Avalonia.Android/AndroidInputMethod.cs rename to src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs index 3f951d6bb1..cb105197cb 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs @@ -5,10 +5,9 @@ using Android.Runtime; using Android.Text; using Android.Views; using Android.Views.InputMethods; -using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Input.TextInput; -namespace Avalonia.Android +namespace Avalonia.Android.Platform.Input { internal interface IAndroidInputMethod { @@ -21,18 +20,18 @@ namespace Avalonia.Android public InputMethodManager IMM { get; } - void OnBatchEditedEnded(); + void OnBatchEditEnded(); } enum CustomImeFlags - { + { ActionNone = 0x00000001, - ActionGo = 0x00000002, - ActionSearch = 0x00000003, - ActionSend = 0x00000004, - ActionNext = 0x00000005, - ActionDone = 0x00000006, - ActionPrevious = 0x00000007, + ActionGo = 0x00000002, + ActionSearch = 0x00000003, + ActionSend = 0x00000004, + ActionNext = 0x00000005, + ActionDone = 0x00000006, + ActionPrevious = 0x00000007, } internal class AndroidInputMethod : ITextInputMethodImpl, IAndroidInputMethod @@ -79,25 +78,11 @@ namespace Avalonia.Android { _host.RequestFocus(); - _imm.RestartInput(View); + _imm.RestartInput(View); _imm.ShowSoftInput(_host, ShowFlags.Implicit); - var selection = Client.Selection; - - _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); - - var surroundingText = _client.SurroundingText ?? ""; - - var extractedText = new ExtractedText - { - Text = new Java.Lang.String(surroundingText), - SelectionStart = selection.Start, - SelectionEnd = selection.End, - PartialEndOffset = surroundingText.Length - }; - - _imm.UpdateExtractedText(_host, _inputConnection?.ExtractedTextToken ?? 0, extractedText); + _inputConnection?.UpdateState(); _client.SurroundingTextChanged += _client_SurroundingTextChanged; _client.SelectionChanged += _client_SelectionChanged; @@ -110,23 +95,29 @@ namespace Avalonia.Android private void _client_SelectionChanged(object? sender, EventArgs e) { - if (_inputConnection is null || _inputConnection.IsInBatchEdit) + if (_inputConnection is null || _inputConnection.IsInBatchEdit || _inputConnection.IsInUpdate) return; OnSelectionChanged(); } private void OnSelectionChanged() { - if (Client is null) + if (Client is null || _inputConnection is null || _inputConnection.IsInUpdate) { return; } + OnSurroundingTextChanged(); + + _inputConnection.IsInUpdate = true; + var selection = Client.Selection; - _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); + var composition = _inputConnection.EditBuffer.HasComposition ? _inputConnection.EditBuffer.Composition!.Value : new TextSelection(-1,-1); + + _imm.UpdateSelection(_host, selection.Start, selection.End, composition.Start, composition.End); - _inputConnection?.SetSelection(selection.Start, selection.End); + _inputConnection.IsInUpdate = false; } private void _client_SurroundingTextChanged(object? sender, EventArgs e) @@ -136,67 +127,21 @@ namespace Avalonia.Android OnSurroundingTextChanged(); } - public void OnBatchEditedEnded() + public void OnBatchEditEnded() { if (_inputConnection is null || _inputConnection.IsInBatchEdit) return; - - OnSurroundingTextChanged(); OnSelectionChanged(); } private void OnSurroundingTextChanged() { - if(_client is null || _inputConnection is null) - { - return; - } - - var surroundingText = _client.SurroundingText ?? ""; - var editableText = _inputConnection.EditableWrapper.ToString(); - - if (editableText != surroundingText) - { - _inputConnection.EditableWrapper.IgnoreChange = true; - - var diff = GetDiff(); - - _inputConnection.Editable.Replace(diff.index, editableText.Length, diff.diff); - - _inputConnection.EditableWrapper.IgnoreChange = false; - - if(diff.index == 0) - { - var selection = _client.Selection; - _client.Selection = new TextSelection(selection.Start, 0); - _client.Selection = selection; - } - } - - (int index, string diff) GetDiff() - { - int index = 0; - - var longerLength = Math.Max(surroundingText.Length, editableText.Length); - - for (int i = 0; i < longerLength; i++) - { - if (surroundingText.Length == i || editableText.Length == i || surroundingText[i] != editableText[i]) - { - index = i; - break; - } - } - - var diffString = surroundingText.Substring(index, surroundingText.Length - index); - - return (index, diffString); - } + _inputConnection?.UpdateState(); } public void SetCursorRect(Rect rect) { - + } public void SetOptions(TextInputOptions options) @@ -210,22 +155,22 @@ namespace Avalonia.Android outAttrs.InputType = options.ContentType switch { - TextInputContentType.Email => global::Android.Text.InputTypes.TextVariationEmailAddress, - TextInputContentType.Number => global::Android.Text.InputTypes.ClassNumber, - TextInputContentType.Password => global::Android.Text.InputTypes.TextVariationPassword, - TextInputContentType.Digits => global::Android.Text.InputTypes.ClassPhone, - TextInputContentType.Url => global::Android.Text.InputTypes.TextVariationUri, - _ => global::Android.Text.InputTypes.ClassText + TextInputContentType.Email => InputTypes.TextVariationEmailAddress, + TextInputContentType.Number => InputTypes.ClassNumber, + TextInputContentType.Password => InputTypes.TextVariationPassword, + TextInputContentType.Digits => InputTypes.ClassPhone, + TextInputContentType.Url => InputTypes.TextVariationUri, + _ => InputTypes.ClassText }; if (options.AutoCapitalization) { - outAttrs.InitialCapsMode = global::Android.Text.CapitalizationMode.Sentences; - outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagCapSentences; + outAttrs.InitialCapsMode = CapitalizationMode.Sentences; + outAttrs.InputType |= InputTypes.TextFlagCapSentences; } if (options.Multiline) - outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine; + outAttrs.InputType |= InputTypes.TextFlagMultiLine; outAttrs.ImeOptions = options.ReturnKeyType switch { diff --git a/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs b/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs new file mode 100644 index 0000000000..2169936b56 --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs @@ -0,0 +1,334 @@ +using System.Collections.Concurrent; +using System.Threading; +using Android.OS; +using Android.Runtime; +using Android.Text; +using Android.Views; +using Android.Views.InputMethods; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Input; +using Avalonia.Input.TextInput; +using Java.Lang; + +namespace Avalonia.Android.Platform.Input +{ + internal class AvaloniaInputConnection : Object, IInputConnection + { + private readonly TopLevelImpl _toplevel; + private readonly IAndroidInputMethod _inputMethod; + private readonly TextEditBuffer _editBuffer; + private readonly ConcurrentQueue _commandQueue; + + private int _batchLevel = 0; + + public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) + { + _toplevel = toplevel; + _inputMethod = inputMethod; + _editBuffer = new TextEditBuffer(_inputMethod, toplevel); + _commandQueue = new ConcurrentQueue(); + } + + public int ExtractedTextToken { get; private set; } + + public IAndroidInputMethod InputMethod => _inputMethod; + + public TopLevelImpl Toplevel => _toplevel; + + public bool IsInBatchEdit => _batchLevel > 0; + public bool IsInMonitorMode { get; private set; } + + public Handler? Handler => null; + + public TextEditBuffer EditBuffer => _editBuffer; + + public bool IsInUpdate { get; set; } + + internal void UpdateState() + { + var selection = _editBuffer.Selection; + + if (IsInMonitorMode && InputMethod.Client is { } client) + { + InputMethod.IMM.UpdateExtractedText(InputMethod.View, ExtractedTextToken, + _editBuffer.ExtractedText); + } + + var composition = _editBuffer.HasComposition ? _editBuffer.Composition!.Value : new TextSelection(-1, -1); + InputMethod.IMM.UpdateSelection(InputMethod.View, selection.Start, selection.End, composition.Start, composition.End); + } + + public bool SetComposingRegion(int start, int end) + { + if (InputMethod.IsActive) + { + QueueCommand(new CompositionRegionCommand(start, end)); + } + return InputMethod.IsActive; + } + + public bool SetComposingText(ICharSequence? text, int newCursorPosition) + { + if (text is null) + { + return false; + } + + if (InputMethod.IsActive) + { + var compositionText = text.SubSequence(0, text.Length()); + QueueCommand(new CompositionTextCommand(compositionText, newCursorPosition)); + } + + return InputMethod.IsActive; + } + + public bool SetSelection(int start, int end) + { + if (InputMethod.IsActive) + { + if (IsInUpdate) + new SelectionCommand(start, end).Apply(EditBuffer); + else + QueueCommand(new SelectionCommand(start, end)); + } + + return InputMethod.IsActive; + } + + public bool BeginBatchEdit() + { + _batchLevel = Interlocked.Increment(ref _batchLevel); + return InputMethod.IsActive; + } + + public bool EndBatchEdit() + { + _batchLevel = Interlocked.Decrement(ref _batchLevel); + + if (!IsInBatchEdit) + { + IsInUpdate = true; + while (_commandQueue.TryDequeue(out var command)) + { + command.Apply(_editBuffer); + } + IsInUpdate = false; + } + + UpdateState(); + return IsInBatchEdit; + } + + public bool CommitText(ICharSequence? text, int newCursorPosition) + { + if (InputMethod.Client is null || text is null) + { + return false; + } + + if (InputMethod.IsActive) + { + var committedText = text.SubSequence(0, text.Length()); + QueueCommand(new CommitTextCommand(committedText, newCursorPosition)); + } + + return InputMethod.IsActive; + } + + public bool DeleteSurroundingText(int beforeLength, int afterLength) + { + if (InputMethod.IsActive) + { + QueueCommand(new DeleteRegionCommand(beforeLength, afterLength)); + } + + return InputMethod.IsActive; + } + + public bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) + { + switch (actionCode) + { + case ImeAction.Done: + { + _inputMethod.IMM.HideSoftInputFromWindow(_inputMethod.View.WindowToken, HideSoftInputFlags.ImplicitOnly); + break; + } + case ImeAction.Next: + { + FocusManager.GetFocusManager(_toplevel.InputRoot)? + .TryMoveFocus(NavigationDirection.Next); + break; + } + } + + var eventTime = SystemClock.UptimeMillis(); + SendKeyEvent(new KeyEvent(eventTime, + eventTime, + KeyEventActions.Down, + Keycode.Enter, + 0, + 0, + 0, + 0, + KeyEventFlags.SoftKeyboard | KeyEventFlags.KeepTouchMode | KeyEventFlags.EditorAction)); + SendKeyEvent(new KeyEvent(eventTime, + eventTime, + KeyEventActions.Up, + Keycode.Enter, + 0, + 0, + 0, + 0, + KeyEventFlags.SoftKeyboard | KeyEventFlags.KeepTouchMode | KeyEventFlags.EditorAction)); + + return InputMethod.IsActive; + } + + public ExtractedText? GetExtractedText(ExtractedTextRequest? request, [GeneratedEnum] GetTextFlags flags) + { + IsInMonitorMode = ((int)flags & (int)TextExtractFlags.Monitor) != 0; + + ExtractedTextToken = IsInMonitorMode ? request?.Token ?? 0 : ExtractedTextToken; + + if (!_inputMethod.IsActive) + { + return null; + } + + return _editBuffer.ExtractedText; + } + + public bool PerformContextMenuAction(int id) + { + if (InputMethod.Client is not { } client) + return false; + + switch (id) + { + case global::Android.Resource.Id.SelectAll: + client.ExecuteContextMenuAction(ContextMenuAction.SelectAll); + return true; + case global::Android.Resource.Id.Cut: + client.ExecuteContextMenuAction(ContextMenuAction.Cut); + return true; + case global::Android.Resource.Id.Copy: + client.ExecuteContextMenuAction(ContextMenuAction.Copy); + return true; + case global::Android.Resource.Id.Paste: + client.ExecuteContextMenuAction(ContextMenuAction.Paste); + return true; + default: + break; + } + return InputMethod.IsActive; + } + + public bool ClearMetaKeyStates([GeneratedEnum] MetaKeyStates states) + { + return false; + } + + public void CloseConnection() + { + _commandQueue.Clear(); + _batchLevel = 0; + } + + public bool CommitCompletion(CompletionInfo? text) + { + return false; + } + + public bool CommitContent(InputContentInfo inputContentInfo, [GeneratedEnum] InputContentFlags flags, Bundle? opts) + { + return false; + } + + public bool CommitCorrection(CorrectionInfo? correctionInfo) + { + return false; + } + + public bool DeleteSurroundingTextInCodePoints(int beforeLength, int afterLength) + { + if (InputMethod.IsActive) + { + QueueCommand(new DeleteRegionInCodePointsCommand(beforeLength, afterLength)); + } + + return InputMethod.IsActive; + } + + public bool FinishComposingText() + { + if (InputMethod.IsActive) + { + QueueCommand(new FinishComposingCommand()); + } + + return InputMethod.IsActive; + } + + [return: GeneratedEnum] + public CapitalizationMode GetCursorCapsMode([GeneratedEnum] CapitalizationMode reqModes) + { + return TextUtils.GetCapsMode(_editBuffer.Text, _editBuffer.Selection.Start, reqModes); + } + + public ICharSequence? GetSelectedTextFormatted([GeneratedEnum] GetTextFlags flags) + { + return new SpannableString(_editBuffer.SelectedText); + } + + public ICharSequence? GetTextAfterCursorFormatted(int n, [GeneratedEnum] GetTextFlags flags) + { + var end = Math.Min(_editBuffer.Selection.End, _editBuffer.Text.Length); + return new SpannableString(_editBuffer.Text.Substring(end, Math.Min(n, _editBuffer.Text.Length - end))); + } + + public ICharSequence? GetTextBeforeCursorFormatted(int n, [GeneratedEnum] GetTextFlags flags) + { + var start = Math.Max(0, _editBuffer.Selection.Start - n); + var length = _editBuffer.Selection.Start - start; + return _editBuffer.Text == null ? null : new SpannableString(_editBuffer.Text.Substring(start, length)); + } + + public bool PerformPrivateCommand(string? action, Bundle? data) + { + return false; + } + + public bool ReportFullscreenMode(bool enabled) + { + return false; + } + + public bool RequestCursorUpdates(int cursorUpdateMode) + { + return false; + } + + public bool SendKeyEvent(KeyEvent? e) + { + _inputMethod.View.DispatchKeyEvent(e); + + return true; + } + + private void QueueCommand(EditCommand command) + { + BeginBatchEdit(); + + try + { + _commandQueue.Enqueue(command); + } + finally + { + EndBatchEdit(); + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/Input/EditCommand.cs b/src/Android/Avalonia.Android/Platform/Input/EditCommand.cs new file mode 100644 index 0000000000..c110807dd9 --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/Input/EditCommand.cs @@ -0,0 +1,180 @@ +using System; +using Avalonia.Input.TextInput; + +namespace Avalonia.Android.Platform.Input +{ + internal abstract class EditCommand + { + public abstract void Apply(TextEditBuffer buffer); + } + + internal class SelectionCommand : EditCommand + { + private readonly int _start; + private readonly int _end; + + public SelectionCommand(int start, int end) + { + _start = Math.Min(start, end); + _end = Math.Max(start, end); + } + + public override void Apply(TextEditBuffer buffer) + { + var start = Math.Clamp(_start, 0, buffer.Text.Length); + var end = Math.Clamp(_end, 0, buffer.Text.Length); + buffer.Selection = new TextSelection(start, end); + } + } + + internal class CompositionRegionCommand : EditCommand + { + private readonly int _start; + private readonly int _end; + + public CompositionRegionCommand(int start, int end) + { + _start = Math.Min(start, end); + _end = Math.Max(start, end); + } + + public override void Apply(TextEditBuffer buffer) + { + buffer.Composition = new TextSelection(_start, _end); + } + } + + internal class DeleteRegionCommand : EditCommand + { + private readonly int _before; + private readonly int _after; + + public DeleteRegionCommand(int before, int after) + { + _before = before; + _after = after; + } + + public override void Apply(TextEditBuffer buffer) + { + var end = Math.Min(buffer.Text.Length, buffer.Selection.End + _after); + var endCount = end - buffer.Selection.End; + var start = Math.Max(0, buffer.Selection.Start - _before); + buffer.Remove(buffer.Selection.End, endCount); + buffer.Remove(start, buffer.Selection.Start - start); + buffer.Selection = new TextSelection(start, start); + } + } + + internal class DeleteRegionInCodePointsCommand : EditCommand + { + private readonly int _before; + private readonly int _after; + + public DeleteRegionInCodePointsCommand(int before, int after) + { + _before = before; + _after = after; + } + + public override void Apply(TextEditBuffer buffer) + { + var beforeLengthInChar = 0; + + for (int i = 0; i < _before; i++) + { + beforeLengthInChar++; + if (buffer.Selection.Start > beforeLengthInChar) + { + var lead = buffer.Text[buffer.Selection.Start - beforeLengthInChar - 1]; + var trail = buffer.Text[buffer.Selection.Start - beforeLengthInChar]; + + if (char.IsSurrogatePair(lead, trail)) + { + beforeLengthInChar++; + } + } + + if (beforeLengthInChar == buffer.Selection.Start) + break; + } + + var afterLengthInChar = 0; + for (int i = 0; i < _after; i++) + { + afterLengthInChar++; + if (buffer.Selection.End > afterLengthInChar) + { + var lead = buffer.Text[buffer.Selection.End + afterLengthInChar - 1]; + var trail = buffer.Text[buffer.Selection.End + afterLengthInChar]; + + if (char.IsSurrogatePair(lead, trail)) + { + afterLengthInChar++; + } + } + + if (buffer.Selection.End + afterLengthInChar == buffer.Text.Length) + break; + } + + var start = buffer.Selection.Start - beforeLengthInChar; + buffer.Remove(buffer.Selection.End, afterLengthInChar); + buffer.Remove(start, beforeLengthInChar); + buffer.Selection = new TextSelection(start, start); + } + } + + internal class CompositionTextCommand : EditCommand + { + private readonly string _text; + private readonly int _newCursorPosition; + + public CompositionTextCommand(string text, int newCursorPosition) + { + _text = text; + _newCursorPosition = newCursorPosition; + } + + public override void Apply(TextEditBuffer buffer) + { + buffer.ComposingText = _text; + var newCursor = _newCursorPosition > 0 ? buffer.Selection.Start + _newCursorPosition - 1 : buffer.Selection.Start + _newCursorPosition; + buffer.Selection = new TextSelection(newCursor, newCursor); + } + } + + internal class CommitTextCommand : EditCommand + { + private readonly string _text; + private readonly int _newCursorPosition; + + public CommitTextCommand(string text, int newCursorPosition) + { + _text = text; + _newCursorPosition = newCursorPosition; + } + + public override void Apply(TextEditBuffer buffer) + { + if (buffer.HasComposition) + { + buffer.Replace(buffer.Composition!.Value.Start, buffer.Composition!.Value.End, _text); + } + else + { + buffer.Replace(buffer.Selection.Start, buffer.Selection.End, _text); + } + var newCursor = _newCursorPosition > 0 ? buffer.Selection.Start + _newCursorPosition - 1 : buffer.Selection.Start + _newCursorPosition - _text.Length; + buffer.Selection = new TextSelection(newCursor, newCursor); + } + } + + internal class FinishComposingCommand : EditCommand + { + public override void Apply(TextEditBuffer buffer) + { + buffer.Composition = default; + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs b/src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs new file mode 100644 index 0000000000..95c631c10d --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/Input/TextEditBuffer.cs @@ -0,0 +1,122 @@ +using System; +using Android.Text; +using Android.Views; +using Android.Views.InputMethods; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Input.TextInput; + +namespace Avalonia.Android.Platform.Input +{ + internal class TextEditBuffer + { + private readonly IAndroidInputMethod _textInputMethod; + private readonly TopLevelImpl _topLevel; + private TextSelection? _composition; + + public TextEditBuffer(IAndroidInputMethod textInputMethod, TopLevelImpl topLevel) + { + _textInputMethod = textInputMethod; + _topLevel = topLevel; + } + + public bool HasComposition => Composition is { } composition && composition.Start != composition.End; + + public TextSelection Selection + { + get => _textInputMethod.Client?.Selection ?? default; set + { + if (_textInputMethod.Client is { } client) + client.Selection = value; + } + } + + public TextSelection? Composition + { + get => _composition; set + { + if (value is { } v) + { + var text = Text; + var start = Math.Clamp(v.Start, 0, text.Length); + var end = Math.Clamp(v.End, 0, text.Length); + _composition = new TextSelection(start, end); + } + else + _composition = null; + } + } + + public string? SelectedText + { + get + { + if(_textInputMethod.Client is not { } client || Selection.Start < 0 || Selection.End >= client.SurroundingText.Length) + { + return ""; + } + + return client.SurroundingText.Substring(Selection.Start, Selection.End - Selection.Start); + } + } + + public string? ComposingText + { + get => !HasComposition ? null : Text?.Substring(Composition!.Value.Start, Composition!.Value.End - Composition!.Value.Start); set + { + if (HasComposition) + { + var start = Composition!.Value.Start; + Replace(Composition!.Value.Start, Composition!.Value.End, value ?? ""); + Composition = new TextSelection(start, start + (value?.Length ?? 0)); + } + else + { + var start = Selection.Start; + Replace(start, Selection.End, value ?? ""); + Composition = new TextSelection(start, start + (value?.Length ?? 0)); + } + } + } + + public string Text => _textInputMethod.Client?.SurroundingText ?? ""; + + public ExtractedText? ExtractedText => new ExtractedText + { + Flags = Text.Contains('\n') ? 0 : ExtractedTextFlags.SingleLine, + PartialStartOffset = -1, + PartialEndOffset = Text.Length, + SelectionStart = Selection.Start, + SelectionEnd = Selection.End, + StartOffset = 0, + Text = new SpannableString(Text) + }; + + internal void Remove(int index, int length) + { + if (_textInputMethod.Client is { } client) + { + client.Selection = new TextSelection(index, index + length); + if (length > 0) + _textInputMethod?.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); + } + } + + internal void Replace(int start, int end, string text) + { + if (_textInputMethod.Client is { } client) + { + var realStart = Math.Min(start, end); + var realEnd = Math.Max(start, end); + if (realEnd > realStart) + { + client.Selection = new TextSelection(realStart, realEnd); + _textInputMethod?.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); + } + _topLevel.TextInput(text); + var index = realStart + text.Length; + client.Selection = new TextSelection(index, index); + Composition = null; + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 0d9206f418..2ec255df19 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -13,9 +13,12 @@ namespace Avalonia.Android internal abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, INativePlatformHandleSurface { bool _invalidateQueued; + private bool _isDisposed; readonly object _lock = new object(); private readonly Handler _handler; + internal event EventHandler? SurfaceWindowCreated; + IntPtr IPlatformHandle.Handle => Holder?.Surface?.Handle is { } handle ? AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, handle) : default; @@ -39,7 +42,7 @@ namespace Avalonia.Android return; _handler.Post(() => { - if (Holder?.Surface?.IsValid != true) + if (_isDisposed || Holder?.Surface?.IsValid != true) return; try { @@ -53,6 +56,11 @@ namespace Avalonia.Android } } + internal new void Dispose() + { + _isDisposed = true; + } + public void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) { Log.Info("AVALONIA", "Surface Changed"); @@ -62,6 +70,7 @@ namespace Avalonia.Android public void SurfaceCreated(ISurfaceHolder holder) { Log.Info("AVALONIA", "Surface Created"); + SurfaceWindowCreated?.Invoke(this, EventArgs.Empty); DoDraw(); } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index bd4f0e1d78..eac66663a8 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading; using Android.App; using Android.Content; using Android.Graphics; @@ -46,6 +45,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly AndroidInsetsManager? _insetsManager; private readonly ClipboardImpl _clipboard; private readonly AndroidLauncher? _launcher; + private readonly AndroidScreens? _screens; private ViewImpl _view; private WindowTransparencyLevel _transparencyLevel; @@ -55,7 +55,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform { throw new ArgumentException("AvaloniaView.Context must not be null"); } - + _view = new ViewImpl(avaloniaView.Context, this, placeOnTop); _textInputMethod = new AndroidInputMethod(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); @@ -64,6 +64,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _framebuffer = new FramebufferManager(this); _mediaProvider = new AndroidMediaProvider(this); _clipboard = new ClipboardImpl(avaloniaView.Context.GetSystemService(Context.ClipboardService).JavaCast()); + _screens = new AndroidScreens(avaloniaView.Context); RenderScaling = _view.Scaling; @@ -87,7 +88,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public virtual Size ClientSize => _view.Size.ToSize(RenderScaling); public Size? FrameSize => null; - + public Action? Closed { get; set; } public Action? Input { get; set; } @@ -102,6 +103,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform internal InvalidationAwareSurfaceView InternalView => _view; + public double DesktopScaling => RenderScaling; public IPlatformHandle Handle => _view; public IEnumerable Surfaces { get; } @@ -138,7 +140,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform { InputRoot = inputRoot; } - + public virtual void Show() { _view.Visibility = ViewStates.Visible; @@ -150,7 +152,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform { Paint?.Invoke(new Rect(new Point(0, 0), ClientSize)); } - + public virtual void Dispose() { _systemNavigationManager.Dispose(); @@ -270,11 +272,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform } public IPopupImpl? CreatePopup() => null; - + public Action? LostFocus { get; set; } public Action? TransparencyLevelChanged { get; set; } - public WindowTransparencyLevel TransparencyLevel + public WindowTransparencyLevel TransparencyLevel { get => _transparencyLevel; private set @@ -410,6 +412,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform return _launcher; } + if (featureType == typeof(IScreenImpl)) + { + return _screens; + } + return null; } @@ -448,247 +455,4 @@ namespace Avalonia.Android.Platform.SkiaPlatform } } } - - internal class EditableWrapper : SpannableStringBuilder - { - private readonly AvaloniaInputConnection _inputConnection; - - public EditableWrapper(AvaloniaInputConnection inputConnection) - { - _inputConnection = inputConnection; - } - - public TextSelection CurrentSelection => new TextSelection(Selection.GetSelectionStart(this), Selection.GetSelectionEnd(this)); - public TextSelection CurrentComposition => new TextSelection(BaseInputConnection.GetComposingSpanStart(this), BaseInputConnection.GetComposingSpanEnd(this)); - - public bool IgnoreChange { get; set; } - - public override IEditable? Replace(int start, int end, ICharSequence? tb) - { - if (!IgnoreChange && start != end) - { - SelectSurroundingTextForDeletion(start, end); - } - - return base.Replace(start, end, tb); - } - - public override IEditable? Replace(int start, int end, ICharSequence? tb, int tbstart, int tbend) - { - if (!IgnoreChange && start != end) - { - SelectSurroundingTextForDeletion(start, end); - } - - return base.Replace(start, end, tb, tbstart, tbend); - } - - private void SelectSurroundingTextForDeletion(int start, int end) - { - _inputConnection.InputMethod.Client!.Selection = new TextSelection(start, end); - } - } - - internal class AvaloniaInputConnection : BaseInputConnection - { - private readonly TopLevelImpl _toplevel; - private readonly IAndroidInputMethod _inputMethod; - private readonly EditableWrapper _editable; - private bool _commitInProgress; - private int _batchLevel = 0; - - public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true) - { - _toplevel = toplevel; - _inputMethod = inputMethod; - _editable = new EditableWrapper(this); - } - - public int ExtractedTextToken { get; private set; } - - public override IEditable Editable => _editable; - - public EditableWrapper EditableWrapper => _editable; - - public IAndroidInputMethod InputMethod => _inputMethod; - - public TopLevelImpl Toplevel => _toplevel; - - public bool IsInBatchEdit => _batchLevel > 0; - - public override bool SetComposingRegion(int start, int end) - { - return base.SetComposingRegion(start, end); - } - - public override bool SetComposingText(ICharSequence? text, int newCursorPosition) - { - if (InputMethod.Client is null || text is null) - { - return false; - } - - BeginBatchEdit(); - _editable.IgnoreChange = true; - - try - { - if (_editable.CurrentComposition.Start > -1) - { - // Select the composing region. - InputMethod.Client.Selection = new TextSelection(_editable.CurrentComposition.Start, _editable.CurrentComposition.End); - } - var compositionText = text.SubSequence(0, text.Length()); - - if (_inputMethod.IsActive && !_commitInProgress) - { - if (string.IsNullOrEmpty(compositionText)) - _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); - - else - _toplevel.TextInput(compositionText); - } - base.SetComposingText(text, newCursorPosition); - } - finally - { - _editable.IgnoreChange = false; - - EndBatchEdit(); - } - - return true; - } - - public override bool BeginBatchEdit() - { - _batchLevel = Interlocked.Increment(ref _batchLevel); - return base.BeginBatchEdit(); - } - - public override bool EndBatchEdit() - { - _batchLevel = Interlocked.Decrement(ref _batchLevel); - - _inputMethod.OnBatchEditedEnded(); - return base.EndBatchEdit(); - } - - public override bool CommitText(ICharSequence? text, int newCursorPosition) - { - if (InputMethod.Client is null || text is null) - { - return false; - } - - BeginBatchEdit(); - _commitInProgress = true; - - var composingRegion = _editable.CurrentComposition; - - var ret = base.CommitText(text, newCursorPosition); - - if(composingRegion.Start != -1) - { - InputMethod.Client.Selection = composingRegion; - } - - var committedText = text.SubSequence(0, text.Length()); - - if (_inputMethod.IsActive) - if (string.IsNullOrEmpty(committedText)) - _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); - else - _toplevel.TextInput(committedText); - - _commitInProgress = false; - EndBatchEdit(); - - return true; - } - - public override bool DeleteSurroundingText(int beforeLength, int afterLength) - { - if (InputMethod.IsActive) - { - EditableWrapper.IgnoreChange = true; - } - - if (InputMethod.IsActive) - { - var selection = _editable.CurrentSelection; - - InputMethod.Client.Selection = new TextSelection(selection.Start - beforeLength, selection.End + afterLength); - - InputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); - - EditableWrapper.IgnoreChange = true; - } - - return true; - } - - public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) - { - switch (actionCode) - { - case ImeAction.Done: - { - _inputMethod.IMM.HideSoftInputFromWindow(_inputMethod.View.WindowToken, HideSoftInputFlags.ImplicitOnly); - break; - } - case ImeAction.Next: - { - FocusManager.GetFocusManager(_toplevel.InputRoot)? - .TryMoveFocus(NavigationDirection.Next); - break; - } - } - - return base.PerformEditorAction(actionCode); - } - - public override ExtractedText? GetExtractedText(ExtractedTextRequest? request, [GeneratedEnum] GetTextFlags flags) - { - if (request == null) - return null; - - ExtractedTextToken = request.Token; - - var editable = Editable; - - if (editable == null) - { - return null; - } - - if (!_inputMethod.IsActive) - { - return null; - } - - var selection = _editable.CurrentSelection; - - ExtractedText extract = new ExtractedText - { - Flags = 0, - PartialStartOffset = -1, - PartialEndOffset = -1, - SelectionStart = selection.Start, - SelectionEnd = selection.End, - StartOffset = 0 - }; - - if ((request.Flags & GetTextFlags.WithStyles) != 0) - { - extract.Text = new SpannableString(editable); - } - else - { - extract.Text = editable; - } - - return extract; - } - } } diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 952717729f..bb27379a70 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -10,6 +10,7 @@ using Android.Provider; using Android.Webkit; using Avalonia.Logging; using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; using Java.Lang; using AndroidUri = Android.Net.Uri; using Exception = System.Exception; @@ -53,7 +54,8 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem } Activity.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); - return Uri.ToString(); + + return StorageBookmarkHelper.EncodeBookmark(AndroidStorageProvider.AndroidKey, Uri.ToString()!); } public async Task ReleaseBookmarkAsync() diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs index 64c9c3a3cb..54d6afad43 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; using Android; using Android.App; using Android.Content; using Android.Provider; using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; using AndroidUri = Android.Net.Uri; using Exception = System.Exception; using JavaFile = Java.IO.File; @@ -15,6 +17,7 @@ namespace Avalonia.Android.Platform.Storage; internal class AndroidStorageProvider : IStorageProvider { + public static ReadOnlySpan AndroidKey => "android"u8; private readonly Activity _activity; public AndroidStorageProvider(Activity activity) @@ -30,8 +33,8 @@ internal class AndroidStorageProvider : IStorageProvider public Task OpenFolderBookmarkAsync(string bookmark) { - var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark)); - return Task.FromResult(new AndroidStorageFolder(_activity, uri, false)); + var uri = DecodeUriFromBookmark(bookmark); + return Task.FromResult(uri is null ? null : new AndroidStorageFolder(_activity, uri, false)); } public async Task TryGetFileFromPathAsync(Uri filePath) @@ -129,8 +132,19 @@ internal class AndroidStorageProvider : IStorageProvider public Task OpenFileBookmarkAsync(string bookmark) { - var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark)); - return Task.FromResult(new AndroidStorageFile(_activity, uri)); + var uri = DecodeUriFromBookmark(bookmark); + return Task.FromResult(uri is null ? null : new AndroidStorageFile(_activity, uri)); + } + + private static AndroidUri? DecodeUriFromBookmark(string bookmark) + { + return StorageBookmarkHelper.TryDecodeBookmark(AndroidKey, bookmark, out var bytes) switch + { + StorageBookmarkHelper.DecodeResult.Success => AndroidUri.Parse(Encoding.UTF8.GetString(bytes!)), + // Attempt to decode 11.0 android bookmarks + StorageBookmarkHelper.DecodeResult.InvalidFormat => AndroidUri.Parse(bookmark), + _ => null + }; } public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) diff --git a/src/Android/Avalonia.Android/Platform/Vulkan/VulkanNativeInterop.cs b/src/Android/Avalonia.Android/Platform/Vulkan/VulkanNativeInterop.cs new file mode 100644 index 0000000000..d5038fa4d6 --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/Vulkan/VulkanNativeInterop.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.SourceGenerator; +using Avalonia.Vulkan; + +namespace Avalonia.Android.Platform.Vulkan; +partial class AndroidVulkanInterface +{ + public AndroidVulkanInterface(IVulkanInstance instance) + { + Initialize(name => instance.GetInstanceProcAddress(instance.Handle, name)); + } + + [GetProcAddress("vkCreateAndroidSurfaceKHR")] + public partial int vkCreateAndroidSurfaceKHR(IntPtr instance, ref VkAndroidSurfaceCreateInfoKHR pCreateInfo, + IntPtr pAllocator, out ulong pSurface); +} + +struct VkAndroidSurfaceCreateInfoKHR +{ + public const uint VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR = 1000008000; + public uint sType; + public IntPtr pNext; + public uint flags; + public IntPtr window; +} diff --git a/src/Android/Avalonia.Android/Platform/Vulkan/VulkanSupport.cs b/src/Android/Avalonia.Android/Platform/Vulkan/VulkanSupport.cs new file mode 100644 index 0000000000..86f9f5938e --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/Vulkan/VulkanSupport.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Avalonia.Platform; +using Avalonia.Vulkan; + +namespace Avalonia.Android.Platform.Vulkan +{ + internal class VulkanSupport + { + [DllImport("libvulkan.so")] + private static extern IntPtr vkGetInstanceProcAddr(IntPtr instance, string name); + + public static VulkanPlatformGraphics? TryInitialize(VulkanOptions options) => + VulkanPlatformGraphics.TryCreate(options ?? new(), new VulkanPlatformSpecificOptions + { + RequiredInstanceExtensions = { "VK_KHR_android_surface" }, + GetProcAddressDelegate = vkGetInstanceProcAddr, + PlatformFeatures = new Dictionary + { + [typeof(IVulkanKhrSurfacePlatformSurfaceFactory)] = new VulkanSurfaceFactory() + } + }); + + internal class VulkanSurfaceFactory : IVulkanKhrSurfacePlatformSurfaceFactory + { + public bool CanRenderToSurface(IVulkanPlatformGraphicsContext context, object surface) => + surface is INativePlatformHandleSurface handle; + + public IVulkanKhrSurfacePlatformSurface CreateSurface(IVulkanPlatformGraphicsContext context, object handle) => + new AndroidVulkanSurface((INativePlatformHandleSurface)handle); + } + + class AndroidVulkanSurface : IVulkanKhrSurfacePlatformSurface + { + private INativePlatformHandleSurface _handle; + + public AndroidVulkanSurface(INativePlatformHandleSurface handle) + { + _handle = handle; + } + + public double Scaling => _handle.Scaling; + public PixelSize Size => _handle.Size; + public ulong CreateSurface(IVulkanPlatformGraphicsContext context) => + CreateAndroidSurface(_handle.Handle, context.Instance); + + public void Dispose() + { + // No-op + } + } + + private static ulong CreateAndroidSurface(nint handle, IVulkanInstance instance) + { + var vulkanAndroid = new AndroidVulkanInterface(instance); + var createInfo = new VkAndroidSurfaceCreateInfoKHR() + { + + sType = VkAndroidSurfaceCreateInfoKHR.VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR, + window = handle + }; + VulkanException.ThrowOnError("vkCreateAndroidSurfaceKHR", + vulkanAndroid.vkCreateAndroidSurfaceKHR(instance.Handle, ref createInfo, IntPtr.Zero, out var surface)); + return surface; + } + } +} diff --git a/src/Android/Avalonia.Android/SingleViewLifetime.cs b/src/Android/Avalonia.Android/SingleViewLifetime.cs index e71f4e0333..b5dbf38c64 100644 --- a/src/Android/Avalonia.Android/SingleViewLifetime.cs +++ b/src/Android/Avalonia.Android/SingleViewLifetime.cs @@ -10,7 +10,7 @@ internal class SingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLe private AvaloniaMainActivity? _activity; /// - /// Since Main Activity can be swapped, we should adjust litetime as well. + /// Since Main Activity can be swapped, we should adjust lifetime as well. /// public AvaloniaMainActivity Activity { diff --git a/src/Android/Avalonia.Android/Stubs.cs b/src/Android/Avalonia.Android/Stubs.cs index 21df905bb6..c79a23d8e7 100644 --- a/src/Android/Avalonia.Android/Stubs.cs +++ b/src/Android/Avalonia.Android/Stubs.cs @@ -7,6 +7,7 @@ namespace Avalonia.Android internal class WindowingPlatformStub : IWindowingPlatform { public IWindowImpl CreateWindow() => throw new NotSupportedException(); + public ITopLevelImpl CreateEmbeddableTopLevel() => CreateEmbeddableWindow(); public IWindowImpl CreateEmbeddableWindow() => throw new NotSupportedException(); diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs index 8a4469d020..954b62f9bc 100644 --- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Base/Animation/Animators/Animator`1.cs @@ -28,57 +28,70 @@ namespace Avalonia.Animation.Animators if (Count == 0) return neutralValue; - var (beforeKeyFrame, afterKeyFrame) = FindKeyFrames(animationTime); + var (from, to) = GetKeyFrames(animationTime, neutralValue); - double beforeTime, afterTime; - T beforeValue, afterValue; + var progress = (animationTime - from.Time) / (to.Time - from.Time); - if (beforeKeyFrame is null) - { - beforeTime = 0.0; - beforeValue = afterKeyFrame is { FillBefore: true, Value: T fillValue } ? fillValue : neutralValue; - } - else - { - beforeTime = beforeKeyFrame.Cue.CueValue; - beforeValue = beforeKeyFrame.Value is T value ? value : neutralValue; - } - - if (afterKeyFrame is null) - { - afterTime = 1.0; - afterValue = beforeKeyFrame is { FillAfter: true, Value: T fillValue } ? fillValue : neutralValue; - } - else - { - afterTime = afterKeyFrame.Cue.CueValue; - afterValue = afterKeyFrame.Value is T value ? value : neutralValue; - } - - var progress = (animationTime - beforeTime) / (afterTime - beforeTime); - - if (afterKeyFrame?.KeySpline is { } keySpline) + if (to.KeySpline is { } keySpline) progress = keySpline.GetSplineProgress(progress); - return Interpolate(progress, beforeValue, afterValue); + return Interpolate(progress, from.Value, to.Value); } - private (AnimatorKeyFrame? Before, AnimatorKeyFrame? After) FindKeyFrames(double time) + private (KeyFrameInfo From, KeyFrameInfo To) GetKeyFrames(double time, T neutralValue) { Debug.Assert(Count >= 1); - for (var i = 0; i < Count; i++) + // Before or right at the first frame which isn't at time 0.0: interpolate between 0.0 and the first frame. + var firstFrame = this[0]; + var firstTime = firstFrame.Cue.CueValue; + if (time <= firstTime && firstTime > 0.0) { - var keyFrame = this[i]; - var keyFrameTime = keyFrame.Cue.CueValue; + var beforeValue = firstFrame.FillBefore ? GetTypedValue(firstFrame.Value, neutralValue) : neutralValue; + return ( + new KeyFrameInfo(0.0, beforeValue, firstFrame.KeySpline), + KeyFrameInfo.FromKeyFrame(firstFrame, neutralValue)); + } - if (time < keyFrameTime || keyFrameTime == 1.0) - return (i > 0 ? this[i - 1] : null, keyFrame); + // Between two frames: interpolate between the previous frame and the next frame. + for (var i = 1; i < Count; ++i) + { + var frame = this[i]; + if (time <= frame.Cue.CueValue) + { + return ( + KeyFrameInfo.FromKeyFrame(this[i - 1], neutralValue), + KeyFrameInfo.FromKeyFrame(this[i], neutralValue)); + } + } + + // Past the last frame which is at time 1.0: interpolate between the last two frames. + var lastFrame = this[Count - 1]; + if (lastFrame.Cue.CueValue >= 1.0) + { + if (Count == 1) + { + var beforeValue = lastFrame.FillBefore ? GetTypedValue(lastFrame.Value, neutralValue) : neutralValue; + return ( + new KeyFrameInfo(0.0, beforeValue, lastFrame.KeySpline), + KeyFrameInfo.FromKeyFrame(lastFrame, neutralValue)); + } + + return ( + KeyFrameInfo.FromKeyFrame(this[Count - 2], neutralValue), + KeyFrameInfo.FromKeyFrame(lastFrame, neutralValue)); } - return (this[Count - 1], null); + // Past the last frame which isn't at time 1.0: interpolate between the last frame and 1.0. + var afterValue = lastFrame.FillAfter ? GetTypedValue(lastFrame.Value, neutralValue) : neutralValue; + return ( + KeyFrameInfo.FromKeyFrame(lastFrame, neutralValue), + new KeyFrameInfo(1.0, afterValue, lastFrame.KeySpline)); } + private static T GetTypedValue(object? untypedValue, T neutralValue) + => untypedValue is T value ? value : neutralValue; + public virtual IDisposable BindAnimation(Animatable control, IObservable instance) { if (Property is null) @@ -107,5 +120,15 @@ namespace Avalonia.Animation.Animators /// Interpolates in-between two key values given the desired progress time. /// public abstract T Interpolate(double progress, T oldValue, T newValue); + + private readonly struct KeyFrameInfo(double time, T value, KeySpline? keySpline) + { + public readonly double Time = time; + public readonly T Value = value; + public readonly KeySpline? KeySpline = keySpline; + + public static KeyFrameInfo FromKeyFrame(AnimatorKeyFrame source, T neutralValue) + => new(source.Cue.CueValue, GetTypedValue(source.Value, neutralValue), source.KeySpline); + } } } diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt deleted file mode 100644 index 7f378d2f65..0000000000 --- a/src/Avalonia.Base/ApiCompatBaseline.txt +++ /dev/null @@ -1,4 +0,0 @@ -Compat issues with assembly Avalonia.Base: -MembersMustExist : Member 'public System.Int32 System.Int32 Avalonia.Threading.DispatcherPriority.value__' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Threading.IDispatcher.Post(System.Action, T, Avalonia.Threading.DispatcherPriority)' is present in the implementation but not in the contract. -Total Issues: 2 diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 37457ad7c3..853d585232 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -818,11 +818,9 @@ namespace Avalonia var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(this)); property.InvokeSetter(this, fallback); break; - case BindingValueType.DataValidationError: - property.InvokeSetter(this, value); - break; case BindingValueType.Value: case BindingValueType.BindingErrorWithFallback: + case BindingValueType.DataValidationError: case BindingValueType.DataValidationErrorWithFallback: property.InvokeSetter(this, value); break; diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 90465057bd..0017f95583 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -375,7 +375,7 @@ namespace Avalonia return new InstancedBinding(expression, BindingMode.OneWay, BindingPriority.LocalValue); } - BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property, object? anchor) + BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty? property, object? anchor) { return new UntypedObservableBindingExpression(_source, BindingPriority.LocalValue); } diff --git a/src/Avalonia.Base/Metadata/NullableAttributes.cs b/src/Avalonia.Base/Compatibility/NullableAttributes.cs similarity index 62% rename from src/Avalonia.Base/Metadata/NullableAttributes.cs rename to src/Avalonia.Base/Compatibility/NullableAttributes.cs index b6f0f3a47c..39e0f39e59 100644 --- a/src/Avalonia.Base/Metadata/NullableAttributes.cs +++ b/src/Avalonia.Base/Compatibility/NullableAttributes.cs @@ -1,63 +1,30 @@ -#pragma warning disable MA0048 // File name must match type name -#define INTERNAL_NULLABLE_ATTRIBUTES - -// https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs +// https://raw.githubusercontent.com/dotnet/runtime/753648476b64946b8f5f4d25e1294adbabd3165b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. namespace System.Diagnostics.CodeAnalysis { -#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 +#if !NET6_0_OR_GREATER /// Specifies that null is allowed as an input even if the corresponding type disallows it. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class AllowNullAttribute : Attribute - { } + internal sealed class AllowNullAttribute : Attribute { } /// Specifies that null is disallowed as an input even if the corresponding type allows it. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class DisallowNullAttribute : Attribute - { } + internal sealed class DisallowNullAttribute : Attribute { } /// Specifies that an output may be null even if the corresponding type disallows it. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class MaybeNullAttribute : Attribute - { } + internal sealed class MaybeNullAttribute : Attribute { } - /// Specifies that an output will not be null even if the corresponding type allows it. + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class NotNullAttribute : Attribute - { } + internal sealed class NotNullAttribute : Attribute { } /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class MaybeNullWhenAttribute : Attribute + internal sealed class MaybeNullWhenAttribute : Attribute { /// Initializes the attribute with the specified return value condition. /// @@ -71,12 +38,7 @@ namespace System.Diagnostics.CodeAnalysis /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class NotNullWhenAttribute : Attribute + internal sealed class NotNullWhenAttribute : Attribute { /// Initializes the attribute with the specified return value condition. /// @@ -90,12 +52,7 @@ namespace System.Diagnostics.CodeAnalysis /// Specifies that the output will be non-null if the named parameter is non-null. [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class NotNullIfNotNullAttribute : Attribute + internal sealed class NotNullIfNotNullAttribute : Attribute { /// Initializes the attribute with the associated parameter name. /// @@ -109,22 +66,11 @@ namespace System.Diagnostics.CodeAnalysis /// Applied to a method that will never return under any circumstance. [AttributeUsage(AttributeTargets.Method, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class DoesNotReturnAttribute : Attribute - { } + internal sealed class DoesNotReturnAttribute : Attribute { } /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class DoesNotReturnIfAttribute : Attribute + internal sealed class DoesNotReturnIfAttribute : Attribute { /// Initializes the attribute with the specified parameter value. /// @@ -136,82 +82,62 @@ namespace System.Diagnostics.CodeAnalysis /// Gets the condition parameter value. public bool ParameterValue { get; } } -#endif // NETSTANDARD2_0 attributes -#if NETSTANDARD2_1 || NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 - /// - /// Specifies that the method or property will ensure that the listed field and property members have - /// not- values. - /// + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class MemberNotNullAttribute : Attribute + internal sealed class MemberNotNullAttribute : Attribute { - /// Gets field or property member names. - public string[] Members { get; } - /// Initializes the attribute with a field or property member. - /// The field or property member that is promised to be not-null. - public MemberNotNullAttribute(string member) - { - Members = new[] { member }; - } + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = [member]; /// Initializes the attribute with the list of field and property members. - /// The list of field and property members that are promised to be not-null. - public MemberNotNullAttribute(params string[] members) - { - Members = members; - } - } - - /// - /// Specifies that the method or property will ensure that the listed field and property members have - /// non- values when returning with the specified return value condition. - /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] -#if INTERNAL_NULLABLE_ATTRIBUTES - internal -#else - public -#endif - sealed class MemberNotNullWhenAttribute : Attribute - { - /// Gets the return value condition. - public bool ReturnValue { get; } + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; /// Gets field or property member names. public string[] Members { get; } + } + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { /// Initializes the attribute with the specified return value condition and a field or property member. /// - /// The return value condition. If the method returns this value, - /// the associated parameter will not be . + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. /// - /// The field or property member that is promised to be not-. public MemberNotNullWhenAttribute(bool returnValue, string member) { ReturnValue = returnValue; - Members = new[] { member }; + Members = [member]; } - /// Initializes the attribute with the specified return value condition and list of field and property members. - /// + /// Initializes the attribute with the specified return value condition and list of field and property members. /// - /// The return value condition. If the method returns this value, - /// the associated parameter will not be . + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. /// - /// The list of field and property members that are promised to be not-null. public MemberNotNullWhenAttribute(bool returnValue, params string[] members) { ReturnValue = returnValue; Members = members; } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } } -#endif // NETSTANDARD2_1 attributes +#endif } - diff --git a/src/Avalonia.Base/Compatibility/TrimmingAttributes.cs b/src/Avalonia.Base/Compatibility/TrimmingAttributes.cs index 8adc6f6664..0341d86b65 100644 --- a/src/Avalonia.Base/Compatibility/TrimmingAttributes.cs +++ b/src/Avalonia.Base/Compatibility/TrimmingAttributes.cs @@ -1,4 +1,6 @@ -#pragma warning disable MA0048 // File name must match type name +#nullable enable + +#pragma warning disable MA0048 // File name must match type name // https://github.com/dotnet/runtime/tree/main/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis // Licensed to the .NET Foundation under one or more agreements. diff --git a/src/Avalonia.Base/Controls/Classes.cs b/src/Avalonia.Base/Controls/Classes.cs index e41e1cc488..611cc23992 100644 --- a/src/Avalonia.Base/Controls/Classes.cs +++ b/src/Avalonia.Base/Controls/Classes.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls /// public class Classes : AvaloniaList, IPseudoClasses { - private SafeEnumerableList? _listeners; + private SafeEnumerableHashSet? _listeners; /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 70c3e884e8..748882a450 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -147,6 +147,9 @@ namespace Avalonia.Controls public void AddDeferred(object key, IDeferredContent deferredContent) => Add(key, deferredContent); + public void AddNotSharedDeferred(object key, IDeferredContent deferredContent) + => Add(key, new NotSharedDeferredItem(deferredContent)); + public void Clear() { if (_inner?.Count > 0) @@ -236,12 +239,16 @@ namespace Avalonia.Controls try { _lastDeferredItemKey = key; - _inner[key] = value = deferred.Build(null) switch + value = deferred.Build(null) switch { ITemplateResult t => t.Result, { } v => v, _ => null, }; + if (deferred is not NotSharedDeferredItem) + { + _inner[key] = value; + } } finally { @@ -250,7 +257,6 @@ namespace Avalonia.Controls } return true; } - value = null; return false; } @@ -376,5 +382,11 @@ namespace Avalonia.Controls public DeferredItem(Func factory) => _factory = factory; public object? Build(IServiceProvider? serviceProvider) => _factory(serviceProvider); } + + private sealed class NotSharedDeferredItem(IDeferredContent deferredContent) : IDeferredContent + { + private readonly IDeferredContent _deferredContent = deferredContent ; + public object? Build(IServiceProvider? serviceProvider) => _deferredContent.Build(serviceProvider); + } } } diff --git a/src/Avalonia.Base/Data/BindingChainException.cs b/src/Avalonia.Base/Data/BindingChainException.cs index 517440eb9b..37f9cecc8d 100644 --- a/src/Avalonia.Base/Data/BindingChainException.cs +++ b/src/Avalonia.Base/Data/BindingChainException.cs @@ -60,15 +60,15 @@ namespace Avalonia.Data { if (Expression != null && ExpressionErrorPoint != null) { - return $"An error occured binding to '{Expression}' at '{ExpressionErrorPoint}': '{_message}'"; + return $"An error occurred binding to '{Expression}' at '{ExpressionErrorPoint}': '{_message}'"; } else if (Expression != null) { - return $"An error occured binding to '{Expression}': '{_message}'"; + return $"An error occurred binding to '{Expression}': '{_message}'"; } else { - return $"An error occured in a binding: '{_message}'"; + return $"An error occurred in a binding: '{_message}'"; } } } diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index 0c940a1461..39ef4374aa 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -57,7 +57,6 @@ namespace Avalonia.Data /// The binding value. public BindingNotification(object? value) { - Debug.Assert(value is not BindingNotification); _value = value; } @@ -179,7 +178,7 @@ namespace Avalonia.Data /// to . If is a /// then the value will first be extracted. /// - public static object? UpdateValue(object o, object value) + public static object? UpdateValue(object? o, object value) { if (o is BindingNotification n) { diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 9170fbfaa0..b0a97e5c00 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -101,6 +101,25 @@ namespace Avalonia.Data return Apply(target, property, binding); } + /// + /// Retrieves the that is currently active on the + /// specified property. + /// + /// + /// The from which to retrieve the binding expression. + /// + /// + /// The binding target property from which to retrieve the binding expression. + /// + /// + /// The object that is active on the given property or + /// null if no binding expression is active on the given property. + /// + public static BindingExpressionBase? GetBindingExpressionBase(AvaloniaObject target, AvaloniaProperty property) + { + return target.GetValueStore().GetExpression(property); + } + private sealed class TwoWayBindingDisposable : IDisposable { private readonly IDisposable _toTargetSubscription; diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 6d2872fc82..1765cd5d78 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -459,7 +459,7 @@ namespace Avalonia.Data { if (value is UnsetValueType) { - throw new InvalidOperationException("AvaloniaValue.UnsetValue is not a valid value for BindingValue<>."); + throw new InvalidOperationException("AvaloniaProperty.UnsetValue is not a valid value for BindingValue<>."); } if (value is DoNothingType) diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 3371e510e3..ba0ce7bb9e 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -48,6 +48,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri /// The binding mode. /// The binding priority. /// The format string to use. + /// The target property being bound to. /// The null target value. /// /// A final type converter to be run on the produced value. @@ -65,9 +66,10 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri BindingPriority priority = BindingPriority.LocalValue, string? stringFormat = null, object? targetNullValue = null, + AvaloniaProperty? targetProperty = null, TargetTypeConverter? targetTypeConverter = null, UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.PropertyChanged) - : base(priority, enableDataValidation) + : base(priority, targetProperty, enableDataValidation) { if (mode == BindingMode.Default) throw new ArgumentException("Binding mode cannot be Default.", nameof(mode)); @@ -158,7 +160,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri var source = _nodes[0].Source; for (var i = 0; i < _nodes.Count; ++i) - _nodes[i].SetSource(null, null); + _nodes[i].SetSource(AvaloniaProperty.UnsetValue, null); _nodes[0].SetSource(source, null); } @@ -251,10 +253,6 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri _nodes[nodeIndex + 1].SetSource(value, dataValidationError); WriteTargetValueToSource(); } - else if (value is null) - { - OnNodeError(nodeIndex, "Value is null."); - } else { _nodes[nodeIndex + 1].SetSource(value, dataValidationError); @@ -271,11 +269,11 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri /// The error message. internal void OnNodeError(int nodeIndex, string error) { - // Set the source of all nodes after the one that errored to null. This needs to be done - // for each node individually because setting the source to null will not result in + // Set the source of all nodes after the one that errored to unset. This needs to be done + // for each node individually because setting the source to unset will not result in // OnNodeValueChanged or OnNodeError being called. for (var i = nodeIndex + 1; i < _nodes.Count; ++i) - _nodes[i].SetSource(null, null); + _nodes[i].SetSource(AvaloniaProperty.UnsetValue, null); if (_mode == BindingMode.OneWayToSource) return; @@ -392,7 +390,7 @@ internal partial class BindingExpression : UntypedBindingExpressionBase, IDescri protected override void StopCore() { foreach (var node in _nodes) - node.Reset(); + node.SetSource(AvaloniaProperty.UnsetValue, null); if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource && TryGetTarget(out var target)) diff --git a/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs index 6027676501..66a36c99d6 100644 --- a/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs +++ b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs @@ -1,5 +1,4 @@ using System; -using System.Linq.Expressions; using System.Reflection; namespace Avalonia.Data.Core @@ -40,35 +39,19 @@ namespace Avalonia.Data.Core public class ReflectionClrPropertyInfo : ClrPropertyInfo { - static Action? CreateSetter(PropertyInfo info) - { - if (info.SetMethod == null) - return null; - var target = Expression.Parameter(typeof(object), "target"); - var value = Expression.Parameter(typeof(object), "value"); - return Expression.Lambda>( - Expression.Call(Expression.Convert(target, info.DeclaringType!), info.SetMethod, - Expression.Convert(value, info.SetMethod.GetParameters()[0].ParameterType)), - target, value) - .Compile(); - } - - static Func? CreateGetter(PropertyInfo info) - { - if (info.GetMethod == null) - return null; - var target = Expression.Parameter(typeof(object), "target"); - return Expression.Lambda>( - Expression.Convert(Expression.Call(Expression.Convert(target, info.DeclaringType!), info.GetMethod), - typeof(object)), - target) - .Compile(); - } + private static Action? CreateSetter(PropertyInfo info) + => info.SetMethod is { } setMethod ? + (target, value) => setMethod.Invoke(target, [value]) : + null; + + private static Func? CreateGetter(PropertyInfo info) + => info.GetMethod is { } getMethod ? + target => getMethod.Invoke(target, []) : + null; public ReflectionClrPropertyInfo(PropertyInfo info) : base(info.Name, CreateGetter(info), CreateSetter(info), info.PropertyType) { - } } } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs index a347a1ab72..514cf235ff 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/ArrayIndexerNode.cs @@ -44,8 +44,11 @@ internal sealed class ArrayIndexerNode : ExpressionNode, ISettableNode return false; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is Array array) SetValue(array.GetValue(_indexes)); else diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs index 09f4c9be26..266fbb884a 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/AvaloniaPropertyAccessorNode.cs @@ -38,6 +38,9 @@ internal sealed class AvaloniaPropertyAccessorNode : protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is AvaloniaObject newObject) { WeakEvents.AvaloniaPropertyChanged.Subscribe(newObject, this); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs index db8a8e8080..1c9c7d0294 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/CollectionNodeBase.cs @@ -22,8 +22,11 @@ internal abstract class CollectionNodeBase : ExpressionNode, UpdateValueOrSetError(sender); } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + Subscribe(source); UpdateValue(source); } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs index 14e21d4192..2aa14f12a2 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/DataContextNode.cs @@ -4,8 +4,11 @@ namespace Avalonia.Data.Core.ExpressionNodes; internal sealed class DataContextNode : DataContextNodeBase { - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is IDataContextProvider && source is AvaloniaObject ao) { ao.PropertyChanged += OnPropertyChanged; diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs index e8e6633ab7..7a8ab69e6b 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/ExpressionNode.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; @@ -60,16 +61,6 @@ internal abstract class ExpressionNode BuildString(builder); } - /// - /// Resets the node to its uninitialized state when the is unsubscribed. - /// - public void Reset() - { - SetSource(null, null); - _source = null; - _value = AvaloniaProperty.UnsetValue; - } - /// /// Sets the owner binding. /// @@ -101,28 +92,26 @@ internal abstract class ExpressionNode /// public void SetSource(object? source, Exception? dataValidationError) { - var oldSource = Source; - - if (source == AvaloniaProperty.UnsetValue) - source = null; + if (_source?.TryGetTarget(out var oldSource) != true) + oldSource = AvaloniaProperty.UnsetValue; if (source == oldSource) return; - if (oldSource is not null) + if (oldSource is not null && oldSource != AvaloniaProperty.UnsetValue) Unsubscribe(oldSource); - _source = new(source); - - if (source is null) + if (source == AvaloniaProperty.UnsetValue) { - // If the source is null then the value is null. We explicitly do not want to call + // If the source is unset then the value is unset. We explicitly do not want to call // OnSourceChanged as we don't want to raise errors for subsequent nodes in the // binding change. + _source = null; _value = AvaloniaProperty.UnsetValue; } else { + _source = new(source); try { OnSourceChanged(source, dataValidationError); } catch (Exception e) { SetError(e); } } @@ -187,13 +176,20 @@ internal abstract class ExpressionNode else if (notification.ErrorType == BindingErrorType.DataValidationError) { if (notification.HasValue) - SetValue(notification.Value, notification.Error); + { + if (notification.Value is BindingNotification n) + SetValue(n); + else + SetValue(notification.Value, notification.Error); + } else + { SetDataValidationError(notification.Error!); + } } else { - SetValue(notification.Value, null); + SetValue(notification.Value); } } else @@ -215,24 +211,28 @@ internal abstract class ExpressionNode protected void SetValue(object? value, Exception? dataValidationError = null) { Debug.Assert(value is not BindingNotification); + _value = value; + Owner?.OnNodeValueChanged(Index, value, dataValidationError); + } - if (Owner is null) - return; - - // We raise a change notification if: - // - // - This is the initial value (_value is null) - // - There is a data validation error - // - There is no data validation error, but the owner has one - // - The new value is different to the old value - if (_value is null || - dataValidationError is not null || - (dataValidationError is null && Owner.ErrorType == BindingErrorType.DataValidationError) || - !Equals(value, _value)) + /// + /// Called from to validate that the source + /// is non-null and raise a node error if it is not. + /// + /// The expression node source. + /// + /// True if the source is non-null; otherwise, false. + /// + protected bool ValidateNonNullSource([NotNullWhen(true)] object? source) + { + if (source is null) { - _value = value; - Owner.OnNodeValueChanged(Index, value, dataValidationError); + Owner?.OnNodeError(Index - 1, "Value is null."); + _value = null; + return false; } + + return true; } /// @@ -243,7 +243,7 @@ internal abstract class ExpressionNode /// /// Any data validation error reported by the previous expression node. /// - protected abstract void OnSourceChanged(object source, Exception? dataValidationError); + protected abstract void OnSourceChanged(object? source, Exception? dataValidationError); /// /// When implemented in a derived class, unsubscribes from the previous source. diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs index 7ad0b7ee97..1eb15d2469 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/FuncTransformNode.cs @@ -21,8 +21,11 @@ internal sealed class FuncTransformNode : ExpressionNode // We don't have enough information to add anything here. } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + SetValue(_transform(source)); } } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs index ccf6c76f90..b15f285867 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalAncestorElementNode.cs @@ -56,8 +56,11 @@ internal sealed class LogicalAncestorElementNode : SourceNode return target is ILogical logical && logical.IsAttachedToLogicalTree; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is ILogical logical) { var locator = ControlLocator.Track(logical, _ancestorLevel, _ancestorType); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs index bb65ac16dd..8b1102b463 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/LogicalNotNode.cs @@ -28,14 +28,12 @@ internal sealed class LogicalNotNode : ExpressionNode, ISettableNode return false; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { var v = BindingNotification.ExtractValue(source); if (TryConvert(v, out var value)) - { SetValue(BindingNotification.UpdateValue(source, !value), dataValidationError); - } else SetError($"Unable to convert '{source}' to bool."); } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs index 76ea564320..e0641f1461 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/MethodCommandNode.cs @@ -39,8 +39,11 @@ internal sealed class MethodCommandNode : ExpressionNode, IWeakEventSubscriber

(source); if (_plugin.Start(reference, PropertyName) is { } accessor) diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs index d1ecc13208..533520d35c 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginPropertyAccessorNode.cs @@ -42,8 +42,11 @@ internal sealed class DynamicPluginPropertyAccessorNode : ExpressionNode, IPrope return _accessor?.SetValue(value, BindingPriority.LocalValue) ?? false; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + var reference = new WeakReference(source); if (GetPlugin(source) is { } plugin && diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs index 70b0f710a2..f6c692e685 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/DynamicPluginStreamNode.cs @@ -16,8 +16,11 @@ internal sealed class DynamicPluginStreamNode : ExpressionNode builder.Append('^'); } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + var reference = new WeakReference(source); if (GetPlugin(reference) is { } plugin && diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs index 52526431dc..abc7d80744 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionIndexerNode.cs @@ -52,8 +52,11 @@ internal sealed class ReflectionIndexerNode : CollectionNodeBase, ISettableNode return true; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + _indexes = null; if (GetIndexer(source.GetType(), out _getter, out _setter)) diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs index c973d9d236..8b356603a6 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/Reflection/ReflectionTypeCastNode.cs @@ -19,8 +19,11 @@ internal sealed class ReflectionTypeCastNode : ExpressionNode builder.Append(')'); } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (_targetType.IsInstanceOfType(source)) SetValue(source); else diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs index 19e5a58828..32ccc9e214 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/StreamNode.cs @@ -23,8 +23,11 @@ internal sealed class StreamNode : ExpressionNode, IObserver void IObserver.OnError(Exception error) { } void IObserver.OnNext(object? value) => SetValue(value); - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (_plugin.Start(new(source)) is { } accessor) { _subscription = accessor.Subscribe(this); diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs index 6e81a01cee..3dca520fa7 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/TemplatedParentNode.cs @@ -22,8 +22,11 @@ internal sealed class TemplatedParentNode : SourceNode throw new InvalidOperationException("Cannot find a StyledElement to get a TemplatedParent."); } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is StyledElement newElement) { newElement.PropertyChanged += OnPropertyChanged; diff --git a/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs index 4bca2c8cb4..4cf27d8968 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNodes/VisualAncestorElementNode.cs @@ -56,8 +56,11 @@ internal sealed class VisualAncestorElementNode : SourceNode return target is Visual visual && visual.IsAttachedToVisualTree; } - protected override void OnSourceChanged(object source, Exception? dataValidationError) + protected override void OnSourceChanged(object? source, Exception? dataValidationError) { + if (!ValidateNonNullSource(source)) + return; + if (source is Visual visual) { var locator = VisualLocator.Track(visual, _ancestorLevel, _ancestorType); diff --git a/src/Avalonia.Base/Data/Core/IBinding2.cs b/src/Avalonia.Base/Data/Core/IBinding2.cs index 5e57bedd09..1dcbc15b0c 100644 --- a/src/Avalonia.Base/Data/Core/IBinding2.cs +++ b/src/Avalonia.Base/Data/Core/IBinding2.cs @@ -15,6 +15,6 @@ internal interface IBinding2 : IBinding { BindingExpressionBase Instance( AvaloniaObject target, - AvaloniaProperty targetProperty, + AvaloniaProperty? targetProperty, object? anchor); } diff --git a/src/Avalonia.Base/Data/Core/MultiBindingExpression.cs b/src/Avalonia.Base/Data/Core/MultiBindingExpression.cs new file mode 100644 index 0000000000..cd012f9b21 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/MultiBindingExpression.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Data.Core; + +internal class MultiBindingExpression : UntypedBindingExpressionBase, IBindingExpressionSink +{ + private static readonly object s_uninitialized = new object(); + private readonly IBinding[] _bindings; + private readonly IMultiValueConverter? _converter; + private readonly CultureInfo? _converterCulture; + private readonly object? _converterParameter; + private readonly UntypedBindingExpressionBase?[] _expressions; + private readonly object? _fallbackValue; + private readonly object? _targetNullValue; + private readonly object?[] _values; + private readonly ReadOnlyCollection _valuesView; + + public MultiBindingExpression( + BindingPriority priority, + IList bindings, + IMultiValueConverter? converter, + CultureInfo? converterCulture, + object? converterParameter, + object? fallbackValue, + object? targetNullValue) + : base(priority) + { + _bindings = [.. bindings]; + _converter = converter; + _converterCulture = converterCulture; + _converterParameter = converterParameter; + _expressions = new UntypedBindingExpressionBase[_bindings.Length]; + _fallbackValue = fallbackValue; + _targetNullValue = targetNullValue; + _values = new object?[_bindings.Length]; + _valuesView = new(_values); + +#if NETSTANDARD2_0 + for (var i = 0; i < _bindings.Length; ++i) + _values[i] = s_uninitialized; +#else + Array.Fill(_values, s_uninitialized); +#endif + } + + public override string Description => "MultiBinding"; + + protected override void StartCore() + { + if (!TryGetTarget(out var target)) + throw new AvaloniaInternalException("MultiBindingExpression has no target."); + + for (var i = 0; i < _bindings.Length; ++i) + { + var binding = _bindings[i]; + + if (binding is not IBinding2 b) + throw new NotSupportedException($"Unsupported IBinding implementation '{binding}'."); + + var expression = b.Instance(target, null, null); + + if (expression is not UntypedBindingExpressionBase e) + throw new NotSupportedException($"Unsupported BindingExpressionBase implementation '{expression}'."); + + _expressions[i] = e; + e.AttachAndStart(this, target, null, Priority); + } + } + + protected override void StopCore() + { + for (var i = 0; i < _expressions.Length; ++i) + { + _expressions[i]?.Dispose(); + _expressions[i] = null; + _values[i] = s_uninitialized; + } + } + + void IBindingExpressionSink.OnChanged( + UntypedBindingExpressionBase instance, + bool hasValueChanged, + bool hasErrorChanged, + object? value, + BindingError? error) + { + var i = Array.IndexOf(_expressions, instance); + Debug.Assert(i != -1); + + _values[i] = BindingNotification.ExtractValue(value); + PublishValue(); + } + + void IBindingExpressionSink.OnCompleted(UntypedBindingExpressionBase instance) + { + // Nothing to do here. + } + + private void PublishValue() + { + foreach (var v in _values) + { + if (v == s_uninitialized) + return; + } + + if (_converter is not null) + { + var culture = _converterCulture ?? CultureInfo.CurrentCulture; + var converted = _converter.Convert(_valuesView, TargetType, _converterParameter, culture); + + converted = BindingNotification.ExtractValue(converted); + + if (converted != BindingOperations.DoNothing) + { + if (converted == null) + converted = _targetNullValue; + if (converted == AvaloniaProperty.UnsetValue) + converted = _fallbackValue; + PublishValue(converted); + } + } + else + { + PublishValue(_valuesView); + } + } +} diff --git a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs index 6164d8ec33..5c2d9aaddf 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs @@ -12,7 +12,6 @@ namespace Avalonia.Data.Core.Plugins internal static readonly List s_propertyAccessors = new() { new AvaloniaPropertyAccessorPlugin(), - new ReflectionMethodAccessorPlugin(), new InpcPropertyAccessorPlugin(), }; @@ -29,6 +28,20 @@ namespace Avalonia.Data.Core.Plugins new ObservableStreamPlugin(), }; + static BindingPlugins() + { + // When building with AOT, don't create ReflectionMethodAccessorPlugin instance. + // This branch can be eliminated in compile time with AOT. +#if NET6_0_OR_GREATER + if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) +#else + if (true) +#endif + { + s_propertyAccessors.Insert(1, new ReflectionMethodAccessorPlugin()); + } + } + ///

/// An ordered collection of property accessor plugins that can be used to customize /// the reading and subscription of property values on a type. diff --git a/src/Avalonia.Base/Data/Core/TargetTypeConverter.cs b/src/Avalonia.Base/Data/Core/TargetTypeConverter.cs index 2efc5b42bd..7f934c4e41 100644 --- a/src/Avalonia.Base/Data/Core/TargetTypeConverter.cs +++ b/src/Avalonia.Base/Data/Core/TargetTypeConverter.cs @@ -23,6 +23,11 @@ internal abstract class TargetTypeConverter private class DefaultConverter : TargetTypeConverter { + // TypeDescriptor.GetConverter might require unreferenced code for some generic types. + // But it's normally not the case in Avalonia. Additionally, compiled bindings will preserve referenced types. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.TypeConversionSupressWarningMessage)] + [UnconditionalSuppressMessage("Trimming", "IL2067", Justification = TrimmingMessages.TypeConversionSupressWarningMessage)] + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = TrimmingMessages.TypeConversionSupressWarningMessage)] public override bool TryConvert(object? value, Type type, CultureInfo culture, out object? result) { if (value?.GetType() == type) @@ -66,9 +71,6 @@ internal abstract class TargetTypeConverter return true; } -#pragma warning disable IL2026 -#pragma warning disable IL2067 -#pragma warning disable IL2072 // TODO: TypeConverters are not trimming friendly in some edge cases, we probably need // to make compiled bindings emit conversion code at compile-time. var toTypeConverter = TypeDescriptor.GetConverter(t); @@ -76,16 +78,32 @@ internal abstract class TargetTypeConverter if (toTypeConverter.CanConvertFrom(from)) { - result = toTypeConverter.ConvertFrom(null, culture, value); - return true; + try + { + result = toTypeConverter.ConvertFrom(null, culture, value); + return true; + } + catch + { + result = null; + return false; + } } var fromTypeConverter = TypeDescriptor.GetConverter(from); if (fromTypeConverter.CanConvertTo(t)) { - result = fromTypeConverter.ConvertTo(null, culture, value, t); - return true; + try + { + result = fromTypeConverter.ConvertTo(null, culture, value, t); + return true; + } + catch + { + result = null; + return false; + } } // TODO: This requires reflection: we probably need to make compiled bindings emit @@ -95,11 +113,17 @@ internal abstract class TargetTypeConverter t, OperatorType.Implicit | OperatorType.Explicit) is { } cast) { - result = cast.Invoke(null, new[] { value }); - return true; + try + { + result = cast.Invoke(null, new[] { value }); + return true; + } + catch + { + result = null; + return false; + } } -#pragma warning restore IL2067 -#pragma warning restore IL2026 if (value is IConvertible convertible) { diff --git a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs index 9b6e260f95..6b52bbe259 100644 --- a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs +++ b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs @@ -39,12 +39,16 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase, /// /// The default binding priority for the expression. /// + /// The target property being bound to. /// Whether data validation is enabled. public UntypedBindingExpressionBase( BindingPriority defaultPriority, + AvaloniaProperty? targetProperty = null, bool isDataValidationEnabled = false) { Priority = defaultPriority; + TargetProperty = targetProperty; + TargetType = targetProperty?.PropertyType ?? typeof(object); _isDataValidationEnabled = isDataValidationEnabled; } @@ -86,7 +90,7 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase, /// Gets the target type of the binding expression; that is, the type that values produced by /// the expression should be converted to. /// - public Type TargetType { get; private set; } = typeof(object); + public Type TargetType { get; private set; } AvaloniaProperty IValueEntry.Property => TargetProperty ?? throw new Exception(); @@ -200,7 +204,7 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase, internal void AttachAndStart( IBindingExpressionSink subscriber, AvaloniaObject target, - AvaloniaProperty targetProperty, + AvaloniaProperty? targetProperty, BindingPriority priority) { AttachCore(subscriber, null, target, targetProperty, priority); @@ -257,17 +261,19 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase, IBindingExpressionSink sink, ImmediateValueFrame? frame, AvaloniaObject target, - AvaloniaProperty targetProperty, + AvaloniaProperty? targetProperty, BindingPriority priority) { if (_sink is not null) throw new InvalidOperationException("BindingExpression was already attached."); + if (TargetProperty is not null && TargetProperty != targetProperty) + throw new InvalidOperationException("BindingExpression was already attached to a different property."); _sink = sink; _frame = frame; _target = new(target); TargetProperty = targetProperty; - TargetType = targetProperty.PropertyType; + TargetType = targetProperty?.PropertyType ?? typeof(object); Priority = priority; } @@ -403,6 +409,9 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase, /// The new binding or data validation error. private protected void PublishValue(object? value, BindingError? error = null) { + Debug.Assert(value is not BindingNotification); + Debug.Assert(value != BindingOperations.DoNothing); + if (!IsRunning) return; diff --git a/src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs b/src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs index 1e26caa051..df529d8675 100644 --- a/src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/UntypedObservableBindingExpression.cs @@ -30,5 +30,18 @@ internal class UntypedObservableBindingExpression : UntypedBindingExpressionBase void IObserver.OnCompleted() { } void IObserver.OnError(Exception error) { } - void IObserver.OnNext(object? value) => PublishValue(value); + + void IObserver.OnNext(object? value) + { + if (value is BindingNotification n) + { + var v = n.Value; + var e = n.Error is not null ? new BindingError(n.Error, n.ErrorType) : null; + PublishValue(v, e); + } + else + { + PublishValue(value); + } + } } diff --git a/src/Avalonia.Base/Data/IndexerBinding.cs b/src/Avalonia.Base/Data/IndexerBinding.cs index 02bde6f774..d12beb114b 100644 --- a/src/Avalonia.Base/Data/IndexerBinding.cs +++ b/src/Avalonia.Base/Data/IndexerBinding.cs @@ -31,7 +31,7 @@ namespace Avalonia.Data return new InstancedBinding(expression, Mode, BindingPriority.LocalValue); } - BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty targetProperty, object? anchor) + BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor) { return new IndexerBindingExpression(Source, Property, target, targetProperty, Mode); } diff --git a/src/Avalonia.Base/Data/TemplateBinding.cs b/src/Avalonia.Base/Data/TemplateBinding.cs index db878620b4..2b0e054c07 100644 --- a/src/Avalonia.Base/Data/TemplateBinding.cs +++ b/src/Avalonia.Base/Data/TemplateBinding.cs @@ -5,6 +5,7 @@ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.Logging; +using Avalonia.Metadata; using Avalonia.Styling; namespace Avalonia.Data @@ -20,13 +21,14 @@ namespace Avalonia.Data IDisposable { private bool _isSetterValue; + private bool _hasPublishedValue; public TemplateBinding() : base(BindingPriority.Template) { } - public TemplateBinding(AvaloniaProperty property) + public TemplateBinding([InheritDataTypeFrom(InheritDataTypeFromScopeKind.ControlTemplate)] AvaloniaProperty property) : base(BindingPriority.Template) { Property = property; @@ -64,6 +66,7 @@ namespace Avalonia.Data /// /// Gets or sets the name of the source property on the templated parent. /// + [InheritDataTypeFrom(InheritDataTypeFromScopeKind.ControlTemplate)] public AvaloniaProperty? Property { get; set; } /// @@ -80,7 +83,7 @@ namespace Avalonia.Data return new(target, InstanceCore(), Mode, BindingPriority.Template); } - BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty property, object? anchor) + BindingExpressionBase IBinding2.Instance(AvaloniaObject target, AvaloniaProperty? property, object? anchor) { return InstanceCore(); } @@ -106,6 +109,7 @@ namespace Avalonia.Data protected override void StartCore() { + _hasPublishedValue = false; OnTemplatedParentChanged(); if (TryGetTarget(out var target)) target.PropertyChanged += OnTargetPropertyChanged; @@ -197,11 +201,12 @@ namespace Avalonia.Data value = ConvertToTargetType(value); PublishValue(value, error); + _hasPublishedValue = true; if (Mode == BindingMode.OneTime) Stop(); } - else + else if (_hasPublishedValue) { PublishValue(AvaloniaProperty.UnsetValue); } diff --git a/src/Avalonia.Base/Diagnostics/AppliedStyle.cs b/src/Avalonia.Base/Diagnostics/AppliedStyle.cs deleted file mode 100644 index 90390a85da..0000000000 --- a/src/Avalonia.Base/Diagnostics/AppliedStyle.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Avalonia.Styling; - -namespace Avalonia.Diagnostics -{ - public sealed class AppliedStyle - { - private readonly IStyleInstance _instance; - - internal AppliedStyle(IStyleInstance instance) - { - _instance = instance; - } - - public bool HasActivator => _instance.HasActivator; - public bool IsActive => _instance.IsActive; - public StyleBase Style => (StyleBase)_instance.Source; - } -} diff --git a/src/Avalonia.Base/Diagnostics/IValueFrameDiagnostic.cs b/src/Avalonia.Base/Diagnostics/IValueFrameDiagnostic.cs new file mode 100644 index 0000000000..df48efe04e --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/IValueFrameDiagnostic.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Avalonia.Data; +using Avalonia.Metadata; + +namespace Avalonia.Diagnostics; + +public record ValueEntryDiagnostic(AvaloniaProperty Property, object? Value); + +[Unstable] +[NotClientImplementable] +public interface IValueFrameDiagnostic +{ + public enum FrameType + { + Unknown = 0, + Local, + Theme, + Style, + Template + } + + string? Description { get; } + FrameType Type { get; } + bool IsActive { get; } + BindingPriority Priority { get; } + IEnumerable Values { get; } +} diff --git a/src/Avalonia.Base/Diagnostics/LocalValueFrameDiagnostic.cs b/src/Avalonia.Base/Diagnostics/LocalValueFrameDiagnostic.cs new file mode 100644 index 0000000000..96d72c1480 --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/LocalValueFrameDiagnostic.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Avalonia.Data; + +namespace Avalonia.Diagnostics; + +internal class LocalValueFrameDiagnostic : IValueFrameDiagnostic +{ + public LocalValueFrameDiagnostic(IEnumerable values) + { + Values = values; + } + + public string? Description => null; + public IValueFrameDiagnostic.FrameType Type => IValueFrameDiagnostic.FrameType.Local; + public bool IsActive => true; + public BindingPriority Priority => BindingPriority.LocalValue; + public IEnumerable Values { get; } +} diff --git a/src/Avalonia.Base/Diagnostics/StyleDiagnostics.cs b/src/Avalonia.Base/Diagnostics/StyleDiagnostics.cs deleted file mode 100644 index 90326891c6..0000000000 --- a/src/Avalonia.Base/Diagnostics/StyleDiagnostics.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Styling; - -namespace Avalonia.Diagnostics -{ - /// - /// Contains information about style related diagnostics of a control. - /// - public class StyleDiagnostics - { - /// - /// Currently applied styles. - /// - public IReadOnlyList AppliedStyles { get; } - - public StyleDiagnostics(IReadOnlyList appliedStyles) - { - AppliedStyles = appliedStyles; - } - } -} diff --git a/src/Avalonia.Base/Diagnostics/StyleValueFrameDiagnostic.cs b/src/Avalonia.Base/Diagnostics/StyleValueFrameDiagnostic.cs new file mode 100644 index 0000000000..03d901cee2 --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/StyleValueFrameDiagnostic.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using Avalonia.Data; +using Avalonia.PropertyStore; +using Avalonia.Styling; + +namespace Avalonia.Diagnostics; + +internal class StyleValueFrameDiagnostic : IValueFrameDiagnostic +{ + private readonly StyleInstance _styleInstance; + + internal StyleValueFrameDiagnostic(StyleInstance styleInstance) + { + _styleInstance = styleInstance; + } + + public string? Description => _styleInstance.Source switch + { + Style s => GetFullSelector(s), + ControlTheme t => t.TargetType?.Name, + _ => null + }; + + public IValueFrameDiagnostic.FrameType Type => _styleInstance.Source switch + { + Style => IValueFrameDiagnostic.FrameType.Style, + ControlTheme => IValueFrameDiagnostic.FrameType.Theme, + _ => IValueFrameDiagnostic.FrameType.Unknown + }; + + public bool IsActive => _styleInstance.IsActive(); + public BindingPriority Priority => _styleInstance.FramePriority.ToBindingPriority(); + public IEnumerable Values + { + get + { + foreach (var setter in ((StyleBase)_styleInstance.Source!).Setters) + { + if (setter is Setter { Property: not null } regularSetter) + { + yield return new ValueEntryDiagnostic(regularSetter.Property, regularSetter.Value); + } + } + } + } + + private string GetFullSelector(Style? style) + { + var selectors = new Stack(); + + while (style is not null) + { + if (style.Selector is not null) + { + selectors.Push(style.Selector.ToString()); + } + + style = style.Parent as Style; + } + + return string.Concat(selectors); + } +} diff --git a/src/Avalonia.Base/Diagnostics/StyledElementExtensions.cs b/src/Avalonia.Base/Diagnostics/StyledElementExtensions.cs index d7bcc1aa47..32bbe78218 100644 --- a/src/Avalonia.Base/Diagnostics/StyledElementExtensions.cs +++ b/src/Avalonia.Base/Diagnostics/StyledElementExtensions.cs @@ -1,17 +1,16 @@ -namespace Avalonia.Diagnostics +namespace Avalonia.Diagnostics; + +/// +/// Defines diagnostic extensions on s. +/// +public static class StyledElementExtensions { /// - /// Defines diagnostic extensions on s. + /// Gets a style diagnostics for a . /// - public static class StyledElementExtensions + /// The element. + public static ValueStoreDiagnostic GetValueStoreDiagnostic(this StyledElement styledElement) { - /// - /// Gets a style diagnostics for a . - /// - /// The element. - public static StyleDiagnostics GetStyleDiagnostics(this StyledElement styledElement) - { - return styledElement.GetStyleDiagnosticsInternal(); - } + return styledElement.GetValueStore().GetStoreDiagnostic(); } } diff --git a/src/Avalonia.Base/Diagnostics/TrimmingMessages.cs b/src/Avalonia.Base/Diagnostics/TrimmingMessages.cs index 37c2831a50..ce9c9e187b 100644 --- a/src/Avalonia.Base/Diagnostics/TrimmingMessages.cs +++ b/src/Avalonia.Base/Diagnostics/TrimmingMessages.cs @@ -27,4 +27,5 @@ internal static class TrimmingMessages public const string XamlTypeResolvedRequiresUnreferenceCodeMessage = "XamlTypeResolver might require unreferenced code."; public const string IgnoreNativeAotSupressWarningMessage = "This method is not supported by NativeAOT."; + public const string DesignTimeSupressWarningMessage = "This method is design time only."; } diff --git a/src/Avalonia.Base/Diagnostics/ValueFrameDiagnostic.cs b/src/Avalonia.Base/Diagnostics/ValueFrameDiagnostic.cs new file mode 100644 index 0000000000..9b28cf0cdb --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/ValueFrameDiagnostic.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Avalonia.Data; +using Avalonia.PropertyStore; +using Avalonia.Styling; + +namespace Avalonia.Diagnostics; + +internal sealed class ValueFrameDiagnostic : IValueFrameDiagnostic +{ + private readonly ValueFrame _valueFrame; + + internal ValueFrameDiagnostic(ValueFrame valueFrame) + { + _valueFrame = valueFrame; + } + + public string? Description => (_valueFrame.Owner?.Owner as StyledElement)?.StyleKey.Name; + + public IValueFrameDiagnostic.FrameType Type => IValueFrameDiagnostic.FrameType.Template; + + public bool IsActive => _valueFrame.IsActive(); + public BindingPriority Priority => _valueFrame.FramePriority.ToBindingPriority(); + public IEnumerable Values + { + get + { + for (var i = 0; i < _valueFrame.EntryCount; i++) + { + var entry = _valueFrame.GetEntry(i); + if (entry.HasValue()) + { + yield return new ValueEntryDiagnostic(entry.Property, entry.GetValue()); + } + } + } + } +} diff --git a/src/Avalonia.Base/Diagnostics/ValueStoreDiagnostic.cs b/src/Avalonia.Base/Diagnostics/ValueStoreDiagnostic.cs new file mode 100644 index 0000000000..e4880cdc76 --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/ValueStoreDiagnostic.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Avalonia.Styling; + +namespace Avalonia.Diagnostics; + +public class ValueStoreDiagnostic +{ + /// + /// Currently applied frames. + /// + public IReadOnlyList AppliedFrames { get; } + + internal ValueStoreDiagnostic(IReadOnlyList appliedFrames) + { + AppliedFrames = appliedFrames; + } +} diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index 3efe003710..28e8a38827 100644 --- a/src/Avalonia.Base/DirectProperty.cs +++ b/src/Avalonia.Base/DirectProperty.cs @@ -32,7 +32,6 @@ namespace Avalonia { Getter = getter ?? throw new ArgumentNullException(nameof(getter)); Setter = setter; - IsDirect = true; IsReadOnly = setter is null; } @@ -52,7 +51,6 @@ namespace Avalonia { Getter = getter ?? throw new ArgumentNullException(nameof(getter)); Setter = setter; - IsDirect = true; IsReadOnly = setter is null; } diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index b6b9967177..09f8cbdc7b 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -28,6 +28,7 @@ namespace Avalonia AvaloniaPropertyMetadata metadata) : base(name, ownerType, ownerType, metadata) { + IsDirect = true; Owner = ownerType; } @@ -43,6 +44,7 @@ namespace Avalonia AvaloniaPropertyMetadata metadata) : base(source, ownerType, metadata) { + IsDirect = true; Owner = ownerType; } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index baaf63f112..469ad1e6e9 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using Avalonia.Platform; using Avalonia.Threading; namespace Avalonia.Input.GestureRecognizers @@ -13,7 +14,8 @@ namespace Avalonia.Input.GestureRecognizers private bool _canHorizontallyScroll; private bool _canVerticallyScroll; private bool _isScrollInertiaEnabled; - private int _scrollStartDistance = 30; + private readonly static int s_defaultScrollStartDistance = (int)((AvaloniaLocator.Current?.GetService()?.GetTapSize(PointerType.Touch).Height ?? 10) / 2); + private int _scrollStartDistance = s_defaultScrollStartDistance; private bool _scrolling; private Point _trackedRootPoint; @@ -54,7 +56,7 @@ namespace Avalonia.Input.GestureRecognizers public static readonly DirectProperty ScrollStartDistanceProperty = AvaloniaProperty.RegisterDirect(nameof(ScrollStartDistance), o => o.ScrollStartDistance, (o, v) => o.ScrollStartDistance = v, - unsetValue: 30); + unsetValue: s_defaultScrollStartDistance); /// /// Gets or sets a value indicating whether the content can be scrolled horizontally. diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index 00e478bfdb..156178c061 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -64,23 +64,23 @@ namespace Avalonia.Input public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = RoutedEvent.Register( - "PointerMagnifyGesture", RoutingStrategies.Bubble, typeof(Gestures)); + "PointerTouchPadGestureMagnify", RoutingStrategies.Bubble, typeof(Gestures)); public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = RoutedEvent.Register( - "PointerRotateGesture", RoutingStrategies.Bubble, typeof(Gestures)); + "PointerTouchPadGestureRotate", RoutingStrategies.Bubble, typeof(Gestures)); public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = RoutedEvent.Register( - "PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures)); + "PointerTouchPadGestureSwipe", RoutingStrategies.Bubble, typeof(Gestures)); public static readonly RoutedEvent PinchEvent = RoutedEvent.Register( - "PinchEvent", RoutingStrategies.Bubble, typeof(Gestures)); + "Pinch", RoutingStrategies.Bubble, typeof(Gestures)); public static readonly RoutedEvent PinchEndedEvent = RoutedEvent.Register( - "PinchEndedEvent", RoutingStrategies.Bubble, typeof(Gestures)); + "PinchEnded", RoutingStrategies.Bubble, typeof(Gestures)); public static readonly RoutedEvent PullGestureEvent = RoutedEvent.Register( diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index 9ee8ae9711..122176a127 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Text; +using Avalonia.Input.Platform; using Avalonia.Utilities; namespace Avalonia.Input @@ -9,7 +10,7 @@ namespace Avalonia.Input /// /// Defines a keyboard input combination. /// - public sealed class KeyGesture : IEquatable + public sealed class KeyGesture : IEquatable, IFormattable { private static readonly Dictionary s_keySynonyms = new Dictionary { @@ -95,8 +96,28 @@ namespace Avalonia.Input return new KeyGesture(key, keyModifiers); } - public override string ToString() + public override string ToString() => ToString(null, null); + + /// + /// Returns the current KeyGesture as a string formatted according to the format string and appropriate IFormatProvider + /// + /// The format to use. + /// + /// null or "" or "g"The Invariant format, uses Enum.ToString() to format Keys. + /// "p"Use platform specific formatting as registerd. + /// + /// The IFormatProvider to use. If null, uses the appropriate provider registered in the Avalonia Locator, or Invariant. + /// The formatted string. + /// Thrown if the format string is not null, "", "g", or "p" + public string ToString(string? format, IFormatProvider? formatProvider) { + var formatInfo = format switch + { + null or "" or "g" => KeyGestureFormatInfo.Invariant, + "p" => KeyGestureFormatInfo.GetInstance(formatProvider), + _ => throw new FormatException("Unknown format specifier") + }; + var s = StringBuilderCache.Acquire(); static void Plus(StringBuilder s) @@ -109,29 +130,29 @@ namespace Avalonia.Input if (KeyModifiers.HasAllFlags(KeyModifiers.Control)) { - s.Append("Ctrl"); + s.Append(formatInfo.Ctrl); } if (KeyModifiers.HasAllFlags(KeyModifiers.Shift)) { Plus(s); - s.Append("Shift"); + s.Append(formatInfo.Shift); } if (KeyModifiers.HasAllFlags(KeyModifiers.Alt)) { Plus(s); - s.Append("Alt"); + s.Append(formatInfo.Alt); } if (KeyModifiers.HasAllFlags(KeyModifiers.Meta)) { Plus(s); - s.Append("Cmd"); + s.Append(formatInfo.Meta); } Plus(s); - s.Append(Key); + s.Append(formatInfo.FormatKey(Key)); return StringBuilderCache.GetStringAndRelease(s); } diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index b97036c7dd..fc60551606 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -205,6 +205,7 @@ namespace Avalonia.Input { _pointer.Capture(null); _pointer.CaptureGestureRecognizer(null); + _pointer.IsGestureRecognitionSkipped = false; _lastMouseDownButton = default; } return e.Handled; @@ -304,5 +305,10 @@ namespace Avalonia.Input { return _pointer; } + + internal void PlatformCaptureLost() + { + _pointer.Capture(null); + } } } diff --git a/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs b/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs index 867e80f176..b2a79aa3b9 100644 --- a/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs +++ b/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs @@ -92,6 +92,11 @@ public partial class XYFocus return null; } + if(!(XYFocusHelpers.FindXYSearchRoot(inputElement, keyDeviceType) is InputElement searchRoot)) + { + return null; + } + _instance.SetManifoldsFromBounds(bounds); return _instance.GetNextFocusableElement(direction, inputElement, engagedControl, true, new XYFocusOptions @@ -99,7 +104,7 @@ public partial class XYFocus KeyDeviceType = keyDeviceType, FocusedElementBounds = bounds, UpdateManifold = true, - SearchRoot = owner as InputElement ?? inputElement.GetVisualRoot() as InputElement + SearchRoot = searchRoot }); } diff --git a/src/Avalonia.Base/Input/Navigation/XYFocusHelpers.cs b/src/Avalonia.Base/Input/Navigation/XYFocusHelpers.cs index 1914f6f6c6..7bdcfc8fb9 100644 --- a/src/Avalonia.Base/Input/Navigation/XYFocusHelpers.cs +++ b/src/Avalonia.Base/Input/Navigation/XYFocusHelpers.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.VisualTree; namespace Avalonia.Input; @@ -13,11 +14,25 @@ internal static class XYFocusHelpers { return keyDeviceType switch { - null => true, // programmatic input, allow any subtree. + null => !modes.Equals(XYFocusNavigationModes.Disabled), // programmatic input, allow any subtree except Disabled. KeyDeviceType.Keyboard => modes.HasFlag(XYFocusNavigationModes.Keyboard), KeyDeviceType.Gamepad => modes.HasFlag(XYFocusNavigationModes.Gamepad), KeyDeviceType.Remote => modes.HasFlag(XYFocusNavigationModes.Remote), _ => throw new ArgumentOutOfRangeException(nameof(keyDeviceType), keyDeviceType, null) }; } + + internal static InputElement? FindXYSearchRoot(this InputElement visual, KeyDeviceType? keyDeviceType) + { + InputElement candidate = visual; + InputElement? candidateParent = visual.FindAncestorOfType(); + + while (candidateParent is not null && candidateParent.IsAllowedXYNavigationMode(keyDeviceType)) + { + candidate = candidateParent; + candidateParent = candidate.FindAncestorOfType(); + } + + return candidate; + } } diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs index 128a49e13b..f8cb713e73 100644 --- a/src/Avalonia.Base/Input/PenDevice.cs +++ b/src/Avalonia.Base/Input/PenDevice.cs @@ -167,6 +167,7 @@ namespace Avalonia.Input { pointer.Capture(null); pointer.CaptureGestureRecognizer(null); + pointer.IsGestureRecognitionSkipped = false; _lastMouseDownButton = default; } diff --git a/src/Avalonia.Base/Input/Platform/KeyGestureFormatInfo.cs b/src/Avalonia.Base/Input/Platform/KeyGestureFormatInfo.cs new file mode 100644 index 0000000000..9eb95bf3f8 --- /dev/null +++ b/src/Avalonia.Base/Input/Platform/KeyGestureFormatInfo.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Input.Platform +{ + + /// + /// Provides platform specific formatting information for the KeyGesture class + /// + /// A dictionary of Key to String overrides for specific characters, for example Key.Left to "Left Arrow" or "←" on Mac. + /// A null value is assumed to be the Invariant, so the included set of common overrides will be skipped if this is null. If only the common overrides are + /// desired, pass an empty Dictionary instead. + /// The string to use for the Meta modifier, defaults to "Cmd" + /// The string to use for the Ctrl modifier, defaults to "Ctrl" + /// The string to use for the Alt modifier, defaults to "Alt" + /// The string to use for the Shift modifier, defaults to "Shift" + public sealed class KeyGestureFormatInfo(IReadOnlyDictionary? platformKeyOverrides = null, + string meta = "Cmd", + string ctrl = "Ctrl", + string alt = "Alt", + string shift = "Shift") : IFormatProvider + { + /// + /// The Invariant format. Only uses strings straight from the appropriate Enums. + /// + public static KeyGestureFormatInfo Invariant { get; } = new(); + + /// + /// The string used to represent Meta on the appropriate platform. Defaults to "Cmd". + /// + public string Meta { get; } = meta; + + /// + /// The string used to represent Ctrl on the appropriate platform. Defaults to "Ctrl". + /// + public string Ctrl { get; } = ctrl; + + /// + /// The string used to represent Alt on the appropriate platform. Defaults to "Alt". + /// + public string Alt { get; } = alt; + + /// + /// The string used to represent Shift on the appropriate platform. Defaults to "Shift". + /// + public string Shift { get; } = shift; + + public object? GetFormat(Type? formatType) => formatType == typeof(KeyGestureFormatInfo) ? this : null; + + /// + /// Gets the most appropriate KeyGestureFormatInfo for the IFormatProvider requested. This will be, in order: + /// 1. The provided IFormatProvider as a KeyGestureFormatInfo + /// 2. The currently registered platform specific KeyGestureFormatInfo, if present. + /// 3. The Invariant otherwise. + /// + /// The IFormatProvider to get a KeyGestureFormatInfo for. + /// + public static KeyGestureFormatInfo GetInstance(IFormatProvider? formatProvider) + => formatProvider?.GetFormat(typeof(KeyGestureFormatInfo)) as KeyGestureFormatInfo + ?? AvaloniaLocator.Current.GetService() + ?? Invariant; + + /// + /// A dictionary of the common platform Key overrides. These are used as a fallback + /// if platformKeyOverrides doesn't contain the Key in question. + /// + + private static readonly Dictionary s_commonKeyOverrides = new() + { + { Key.Add , "+" }, + { Key.D0 , "0" }, + { Key.D1 , "1" }, + { Key.D2 , "2" }, + { Key.D3 , "3" }, + { Key.D4 , "4" }, + { Key.D5 , "5" }, + { Key.D6 , "6" }, + { Key.D7 , "7" }, + { Key.D8 , "8" }, + { Key.D9 , "9" }, + { Key.Decimal , "." }, + { Key.Divide , "/" }, + { Key.Multiply , "*" }, + { Key.OemBackslash , "\\" }, + { Key.OemCloseBrackets , "]" }, + { Key.OemComma , "," }, + { Key.OemMinus , "-" }, + { Key.OemOpenBrackets , "[" }, + { Key.OemPeriod , "." }, + { Key.OemPipe , "|" }, + { Key.OemPlus , "+" }, + { Key.OemQuestion , "/" }, + { Key.OemQuotes , "\"" }, + { Key.OemSemicolon , ";" }, + { Key.OemTilde , "`" }, + { Key.Separator , "/" }, + { Key.Subtract , "-" }, + { Key.Back , "Backspace" }, + { Key.Down , "Down Arrow" }, + { Key.Left , "Left Arrow" }, + { Key.Right , "Right Arrow" }, + { Key.Up , "Up Arrow" } + }; + + /// + /// Checks the platformKeyOverrides and s_commonKeyOverrides Dictionaries, in order, for the appropriate + /// string to represent the given Key on this platform. + /// NOTE: If platformKeyOverrides is null, this is assumed to be the Invariant and the Dictionaries are not checked. + /// The plain Enum string is returned instead. + /// + /// The Key to format. + /// The appropriate platform specific or common override if present, key.ToString() if not or this is the Invariant. + public string FormatKey(Key key) + { + /* + * The absence of an Overrides dictionary indicates this is the invariant, and + * so should just return the default ToString() value. + */ + if (platformKeyOverrides == null) + return key.ToString(); + + return platformKeyOverrides.TryGetValue(key, out string? result) ? result : + s_commonKeyOverrides.TryGetValue(key, out string? cresult) ? cresult : + key.ToString() ; + + } + + + } +} diff --git a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs index 61a6d3e30a..ca91b861a8 100644 --- a/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs @@ -64,6 +64,12 @@ namespace Avalonia.Input.TextInput /// public virtual void SetPreeditText(string? preeditText) { } + /// + /// Execute specific context menu actions + /// + /// The to perform + public virtual void ExecuteContextMenuAction(ContextMenuAction action) { } + /// /// Sets the non-committed input string and cursor offset in that string /// @@ -71,6 +77,8 @@ namespace Avalonia.Input.TextInput { SetPreeditText(preeditText); } + + public virtual void ShowInputPanel() { } protected virtual void RaiseTextViewVisualChanged() { @@ -99,4 +107,12 @@ namespace Avalonia.Input.TextInput } public record struct TextSelection(int Start, int End); + + public enum ContextMenuAction + { + Copy, + Cut, + Paste, + SelectAll + } } diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs index ca0af28713..8e662ea1b9 100644 --- a/src/Avalonia.Base/Input/TouchDevice.cs +++ b/src/Avalonia.Base/Input/TouchDevice.cs @@ -119,6 +119,8 @@ namespace Avalonia.Input { pointer?.Capture(null); pointer?.CaptureGestureRecognizer(null); + if (pointer != null) + pointer.IsGestureRecognitionSkipped = false; } } @@ -158,5 +160,11 @@ namespace Avalonia.Input ? pointer : null; } + + internal void PlatformCaptureLost() + { + foreach (var pointer in _pointers.Values) + pointer.Capture(null); + } } } diff --git a/src/Avalonia.Base/Media/ArcSegment.cs b/src/Avalonia.Base/Media/ArcSegment.cs index ee353b0a89..f5807ab9d7 100644 --- a/src/Avalonia.Base/Media/ArcSegment.cs +++ b/src/Avalonia.Base/Media/ArcSegment.cs @@ -97,7 +97,7 @@ namespace Avalonia.Media internal override void ApplyTo(StreamGeometryContext ctx) { - ctx.ArcTo(Point, Size, RotationAngle, IsLargeArc, SweepDirection); + ctx.ArcTo(Point, Size, RotationAngle, IsLargeArc, SweepDirection, IsStroked); } public override string ToString() diff --git a/src/Avalonia.Base/Media/BezierSegment .cs b/src/Avalonia.Base/Media/BezierSegment .cs index 31efe1ec23..1c09081e3b 100644 --- a/src/Avalonia.Base/Media/BezierSegment .cs +++ b/src/Avalonia.Base/Media/BezierSegment .cs @@ -58,7 +58,7 @@ namespace Avalonia.Media internal override void ApplyTo(StreamGeometryContext ctx) { - ctx.CubicBezierTo(Point1, Point2, Point3); + ctx.CubicBezierTo(Point1, Point2, Point3, IsStroked); } public override string ToString() diff --git a/src/Avalonia.Base/Media/Brush.cs b/src/Avalonia.Base/Media/Brush.cs index 21be08b4af..8c3491ed33 100644 --- a/src/Avalonia.Base/Media/Brush.cs +++ b/src/Avalonia.Base/Media/Brush.cs @@ -101,6 +101,8 @@ namespace Avalonia.Media private protected void RegisterForSerialization() => _resource.RegisterForInvalidationOnAllCompositors(this); + private protected bool IsOnCompositor(Compositor c) => _resource.TryGetForCompositor(c) != null; + private CompositorResourceHolder _resource; IBrush ICompositionRenderResource.GetForCompositor(Compositor c) => _resource.GetForCompositor(c); diff --git a/src/Avalonia.Base/Media/DrawingBrush.cs b/src/Avalonia.Base/Media/DrawingBrush.cs index b710351b98..c4e5dc8d13 100644 --- a/src/Avalonia.Base/Media/DrawingBrush.cs +++ b/src/Avalonia.Base/Media/DrawingBrush.cs @@ -58,7 +58,7 @@ namespace Avalonia.Media internal override Func Factory => static c => new ServerCompositionSimpleContentBrush(c.Server); - private InlineDictionary _renderDataDictionary; + private InlineDictionary _renderDataDictionary; private protected override void OnReferencedFromCompositor(Compositor c) { @@ -69,33 +69,27 @@ namespace Avalonia.Media protected override void OnUnreferencedFromCompositor(Compositor c) { if (_renderDataDictionary.TryGetAndRemoveValue(c, out var content)) - content?.RenderData.Dispose(); + content?.Dispose(); base.OnUnreferencedFromCompositor(c); } private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer) { base.SerializeChanges(c, writer); - if (_renderDataDictionary.TryGetValue(c, out var content)) - writer.WriteObject(content); + if (_renderDataDictionary.TryGetValue(c, out var content) && content != null) + writer.WriteObject(new CompositionRenderDataSceneBrushContent.Properties(content.Server, null, true)); else writer.WriteObject(null); } - CompositionRenderDataSceneBrushContent? CreateServerContent(Compositor c) + CompositionRenderData? CreateServerContent(Compositor c) { if (Drawing == null) return null; using var recorder = new RenderDataDrawingContext(c); Drawing?.Draw(recorder); - var renderData = recorder.GetRenderResults(); - if (renderData == null) - return null; - - return new CompositionRenderDataSceneBrushContent( - (ServerCompositionSimpleContentBrush)((ICompositionRenderResource)this).GetForCompositor(c), - renderData, null, true); + return recorder.GetRenderResults(); } } } diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 65ade1a413..88e332f334 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq; +using Avalonia.Logging; using Avalonia.Media.Fonts; using Avalonia.Platform; using Avalonia.Utilities; @@ -96,11 +98,11 @@ namespace Avalonia.Media return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface); } - if (fontFamily.Key is FontFamilyKey) + if (fontFamily.Key != null) { if (fontFamily.Key is CompositeFontFamilyKey compositeKey) { - for (int i = 0; i < compositeKey.Keys.Count; i++) + for (var i = 0; i < compositeKey.Keys.Count; i++) { var key = compositeKey.Keys[i]; @@ -115,7 +117,9 @@ namespace Avalonia.Media } else { - if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, fontFamily.FamilyNames.PrimaryFamilyName, out glyphTypeface)) + var familyName = fontFamily.FamilyNames.PrimaryFamilyName; + + if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, familyName, out glyphTypeface)) { return true; } @@ -125,7 +129,9 @@ namespace Avalonia.Media } else { - if (SystemFonts.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) + var familyName = fontFamily.FamilyNames.PrimaryFamilyName; + + if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) { return true; } @@ -144,13 +150,20 @@ namespace Avalonia.Media { var source = key.Source.EnsureAbsolute(key.BaseUri); - if (TryGetFontCollection(source, out var fontCollection) && - fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface)) + if (TryGetFontCollection(source, out var fontCollection)) { - if (glyphTypeface.FamilyName.Contains(familyName)) + if (fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, + out glyphTypeface)) { return true; } + + var logger = Logger.TryGet(LogEventLevel.Debug, "FontManager"); + + logger?.Log(this, + $"Font family '{familyName}' could not be found. Present font families: [{string.Join(",", fontCollection)}]"); + + return false; } glyphTypeface = null; diff --git a/src/Avalonia.Base/Media/FontWeight.cs b/src/Avalonia.Base/Media/FontWeight.cs index 5a4a4963a5..5776e5e285 100644 --- a/src/Avalonia.Base/Media/FontWeight.cs +++ b/src/Avalonia.Base/Media/FontWeight.cs @@ -1,3 +1,4 @@ +#pragma warning disable CA1069 namespace Avalonia.Media { /// @@ -22,7 +23,7 @@ namespace Avalonia.Media /// /// Specifies an "ultra light" font weight. /// - UltraLight = 200, + UltraLight = ExtraLight, /// /// Specifies a "light" font weight. @@ -42,7 +43,7 @@ namespace Avalonia.Media /// /// Specifies a "regular" font weight. /// - Regular = 400, + Regular = Normal, /// /// Specifies a "medium" font weight. @@ -52,7 +53,7 @@ namespace Avalonia.Media /// /// Specifies a "demi-bold" font weight. /// - DemiBold = 600, + DemiBold = SemiBold, /// /// Specifies a "semi-bold" font weight. @@ -72,7 +73,7 @@ namespace Avalonia.Media /// /// Specifies an "ultra bold" font weight. /// - UltraBold = 800, + UltraBold = ExtraBold, /// /// Specifies a "black" font weight. @@ -82,7 +83,12 @@ namespace Avalonia.Media /// /// Specifies a "heavy" font weight. /// - Heavy = 900, + Heavy = Black, + + /// + /// Specifies a "solid" font weight. + /// + Solid = Black, /// /// Specifies an "extra black" font weight. diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index 49cead719c..d06bad8001 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -45,30 +45,22 @@ namespace Avalonia.Media.Fonts if (fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) { - if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) - { - glyphTypefaces = new ConcurrentDictionary(); - - if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces)) - { - _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName)); - } - } - - var key = new FontCollectionKey( - glyphTypeface.Style, - glyphTypeface.Weight, - glyphTypeface.Stretch); - - glyphTypefaces.TryAdd(key, glyphTypeface); + AddGlyphTypeface(glyphTypeface); } } } - public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) { + var typeface = GetImplicitTypeface(new Typeface(familyName, style, weight, stretch), out familyName); + + style = typeface.Style; + + weight = typeface.Weight; + + stretch = typeface.Stretch; + var key = new FontCollectionKey(style, weight, stretch); if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) @@ -89,7 +81,7 @@ namespace Avalonia.Media.Fonts fontSimulations |= FontSimulations.Oblique; } - if ((int)weight >= 600 && glyphTypeface2.Weight != weight) + if ((int)weight >= 600 && glyphTypeface2.Weight < weight) { fontSimulations |= FontSimulations.Bold; } @@ -134,5 +126,39 @@ namespace Avalonia.Media.Fonts } public override IEnumerator GetEnumerator() => _fontFamilies.GetEnumerator(); + + private void AddGlyphTypeface(IGlyphTypeface glyphTypeface) + { + if (glyphTypeface is IGlyphTypeface2 glyphTypeface2) + { + foreach (var kvp in glyphTypeface2.FamilyNames) + { + var familyName = kvp.Value; + + AddGlyphTypefaceByFamilyName(familyName, glyphTypeface); + } + } + else + { + AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface); + } + + return; + + void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface) + { + var typefaces = _glyphTypefaceCache.GetOrAdd(familyName, + x => + { + _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName)); + + return new ConcurrentDictionary(); + }); + + typefaces.TryAdd( + new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch), + glyphTypeface); + } + } } } diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index 3daa19c788..bbf10fdca8 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media.Fonts { @@ -255,5 +256,124 @@ namespace Avalonia.Media.Fonts return false; } + + internal static Typeface GetImplicitTypeface(Typeface typeface, out string normalizedFamilyName) + { + normalizedFamilyName = typeface.FontFamily.FamilyNames.PrimaryFamilyName; + + //Return early if no separator is present. + if (!normalizedFamilyName.Contains(' ')) + { + return typeface; + } + + var style = typeface.Style; + var weight = typeface.Weight; + var stretch = typeface.Stretch; + + if(TryGetStyle(ref normalizedFamilyName, out var foundStyle)) + { + style = foundStyle; + } + + if(TryGetWeight(ref normalizedFamilyName, out var foundWeight)) + { + weight = foundWeight; + } + + if(TryGetStretch(ref normalizedFamilyName, out var foundStretch)) + { + stretch = foundStretch; + } + + //Preserve old font source + return new Typeface(typeface.FontFamily, style, weight, stretch); + + } + + internal static bool TryGetWeight(ref string familyName, out FontWeight weight) + { + weight = FontWeight.Normal; + + var tokenizer = new StringTokenizer(familyName, ' '); + + tokenizer.ReadString(); + + while (tokenizer.TryReadString(out var weightString)) + { + if (new StringTokenizer(weightString).TryReadInt32(out _)) + { + continue; + } + + if (!Enum.TryParse(weightString, true, out weight)) + { + continue; + } + + familyName = familyName.Replace(" " + weightString, "").TrimEnd(); + + return true; + } + + return false; + } + + internal static bool TryGetStyle(ref string familyName, out FontStyle style) + { + style = FontStyle.Normal; + + var tokenizer = new StringTokenizer(familyName, ' '); + + tokenizer.ReadString(); + + while (tokenizer.TryReadString(out var styleString)) + { + //Do not try to parse an integer + if (new StringTokenizer(styleString).TryReadInt32(out _)) + { + continue; + } + + if (!Enum.TryParse(styleString, true, out style)) + { + continue; + } + + familyName = familyName.Replace(" " + styleString, "").TrimEnd(); + + return true; + } + + return false; + } + + internal static bool TryGetStretch(ref string familyName, out FontStretch stretch) + { + stretch = FontStretch.Normal; + + var tokenizer = new StringTokenizer(familyName, ' '); + + tokenizer.ReadString(); + + while (tokenizer.TryReadString(out var stretchString)) + { + if (new StringTokenizer(stretchString).TryReadInt32(out _)) + { + continue; + } + + if (!Enum.TryParse(stretchString, true, out stretch)) + { + continue; + } + + familyName = familyName.Replace(" " + stretchString, "").TrimEnd(); + + return true; + } + + return false; + } } } diff --git a/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs b/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs new file mode 100644 index 0000000000..b0c725ca92 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs @@ -0,0 +1,71 @@ +using System; + +namespace Avalonia.Media.Fonts +{ + internal readonly record struct OpenTypeTag + { + public static readonly OpenTypeTag None = new OpenTypeTag(0, 0, 0, 0); + public static readonly OpenTypeTag Max = new OpenTypeTag(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); + public static readonly OpenTypeTag MaxSigned = new OpenTypeTag((byte)sbyte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); + + private readonly uint _value; + + public OpenTypeTag(uint value) + { + _value = value; + } + + public OpenTypeTag(char c1, char c2, char c3, char c4) + { + _value = (uint)(((byte)c1 << 24) | ((byte)c2 << 16) | ((byte)c3 << 8) | (byte)c4); + } + + private OpenTypeTag(byte c1, byte c2, byte c3, byte c4) + { + _value = (uint)((c1 << 24) | (c2 << 16) | (c3 << 8) | c4); + } + + public static OpenTypeTag Parse(string tag) + { + if (string.IsNullOrEmpty(tag)) + return None; + + var realTag = new char[4]; + + var len = Math.Min(4, tag.Length); + var i = 0; + for (; i < len; i++) + realTag[i] = tag[i]; + for (; i < 4; i++) + realTag[i] = ' '; + + return new OpenTypeTag(realTag[0], realTag[1], realTag[2], realTag[3]); + } + + public override string ToString() + { + if (_value == None) + { + return nameof(None); + } + if (_value == Max) + { + return nameof(Max); + } + if (_value == MaxSigned) + { + return nameof(MaxSigned); + } + + return string.Concat( + (char)(byte)(_value >> 24), + (char)(byte)(_value >> 16), + (char)(byte)(_value >> 8), + (char)(byte)_value); + } + + public static implicit operator uint(OpenTypeTag tag) => tag._value; + + public static implicit operator OpenTypeTag(uint tag) => new OpenTypeTag(tag); + } +} diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs index c919257eee..ce20ace90c 100644 --- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media.Fonts public SystemFontCollection(FontManager fontManager) { _fontManager = fontManager; - _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().ToList(); + _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().Where(x=> !string.IsNullOrEmpty(x)).ToList(); } public override Uri Key => FontManager.SystemFontsKey; @@ -45,49 +45,94 @@ namespace Avalonia.Media.Fonts { glyphTypeface = null; + var typeface = GetImplicitTypeface(new Typeface(familyName, style, weight, stretch), out familyName); + + style = typeface.Style; + + weight = typeface.Weight; + + stretch = typeface.Stretch; + var key = new FontCollectionKey(style, weight, stretch); - var glyphTypefaces = _glyphTypefaceCache.GetOrAdd(familyName, + if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces)) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) + { + return glyphTypeface != null; + } + } + + glyphTypefaces ??= _glyphTypefaceCache.GetOrAdd(familyName, (_) => new ConcurrentDictionary()); - if (glyphTypefaces.TryGetValue(key, out glyphTypeface)) + //Try to create the glyph typeface via system font manager + if (!_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, + out glyphTypeface)) { - return glyphTypeface != null; + glyphTypefaces.TryAdd(key, null); + + return false; } - if(!_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) || - !glyphTypeface.FamilyName.Contains(familyName)) + var createdKey = + new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch); + + //No exact match + if (createdKey != key) { //Try to find nearest match if possible - TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface); + if (!TryGetNearestMatch(glyphTypefaces, key, out var nearestMatch)) + { + glyphTypeface = nearestMatch; + } + else + { + //Try to create a synthetic glyph typeface + if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, out var syntheticGlyphTypeface)) + { + glyphTypeface = syntheticGlyphTypeface; + } + } } - if(glyphTypeface is IGlyphTypeface2 glyphTypeface2) + glyphTypefaces.TryAdd(key, glyphTypeface); + + return glyphTypeface != null; + } + + private bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, + [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface) + { + if (glyphTypeface is IGlyphTypeface2 glyphTypeface2) { var fontSimulations = FontSimulations.None; - if(style != FontStyle.Normal && glyphTypeface2.Style != style) + if (style != FontStyle.Normal && glyphTypeface2.Style != style) { fontSimulations |= FontSimulations.Oblique; } - if((int)weight >= 600 && glyphTypeface2.Weight != weight) + if ((int)weight >= 600 && glyphTypeface2.Weight < weight) { fontSimulations |= FontSimulations.Bold; } - if(fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream)) + if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream)) { using (stream) { - _fontManager.PlatformImpl.TryCreateGlyphTypeface(stream, fontSimulations, out glyphTypeface); + _fontManager.PlatformImpl.TryCreateGlyphTypeface(stream, fontSimulations, + out syntheticGlyphTypeface); + + return syntheticGlyphTypeface != null; } } } - glyphTypefaces.TryAdd(key, glyphTypeface); + syntheticGlyphTypeface = null; - return glyphTypeface != null; + return false; } public override void Initialize(IFontManagerImpl fontManager) diff --git a/src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs b/src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs new file mode 100644 index 0000000000..bca46b7e8c --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs @@ -0,0 +1,422 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +using System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Avalonia.Media.Fonts.Tables +{ + /// + /// BinaryReader using big-endian encoding. + /// + [DebuggerDisplay("Start: {StartOfStream}, Position: {BaseStream.Position}")] + internal class BigEndianBinaryReader : IDisposable + { + /// + /// Buffer used for temporary storage before conversion into primitives + /// + private readonly byte[] _buffer = new byte[16]; + + private readonly bool _leaveOpen; + + /// + /// Initializes a new instance of the class. + /// Constructs a new binary reader with the given bit converter, reading + /// to the given stream, using the given encoding. + /// + /// Stream to read data from + /// if set to true [leave open]. + public BigEndianBinaryReader(Stream stream, bool leaveOpen) + { + BaseStream = stream; + StartOfStream = stream.Position; + _leaveOpen = leaveOpen; + } + + private long StartOfStream { get; } + + /// + /// Gets the underlying stream of the EndianBinaryReader. + /// + public Stream BaseStream { get; } + + /// + /// Seeks within the stream. + /// + /// Offset to seek to. + /// Origin of seek operation. If SeekOrigin.Begin, the offset will be set to the start of stream position. + public void Seek(long offset, SeekOrigin origin) + { + // If SeekOrigin.Begin, the offset will be set to the start of stream position. + if (origin == SeekOrigin.Begin) + { + offset += StartOfStream; + } + + BaseStream.Seek(offset, origin); + } + + /// + /// Reads a single byte from the stream. + /// + /// The byte read + public byte ReadByte() + { + ReadInternal(_buffer, 1); + return _buffer[0]; + } + + /// + /// Reads a single signed byte from the stream. + /// + /// The byte read + public sbyte ReadSByte() + { + ReadInternal(_buffer, 1); + return unchecked((sbyte)_buffer[0]); + } + + public float ReadF2dot14() + { + const float f2Dot14ToFloat = 16384.0f; + return ReadInt16() / f2Dot14ToFloat; + } + + /// + /// Reads a 16-bit signed integer from the stream, using the bit converter + /// for this reader. 2 bytes are read. + /// + /// The 16-bit integer read + public short ReadInt16() + { + ReadInternal(_buffer, 2); + + return BinaryPrimitives.ReadInt16BigEndian(_buffer); + } + + public TEnum ReadInt16() + where TEnum : struct, Enum + { + TryConvert(ReadUInt16(), out TEnum value); + return value; + } + + public short ReadFWORD() => ReadInt16(); + + public short[] ReadFWORDArray(int length) => ReadInt16Array(length); + + public ushort ReadUFWORD() => ReadUInt16(); + + /// + /// Reads a fixed 32-bit value from the stream. + /// 4 bytes are read. + /// + /// The 32-bit value read. + public float ReadFixed() + { + ReadInternal(_buffer, 4); + return BinaryPrimitives.ReadInt32BigEndian(_buffer) / 65536F; + } + + /// + /// Reads a 32-bit signed integer from the stream, using the bit converter + /// for this reader. 4 bytes are read. + /// + /// The 32-bit integer read + public int ReadInt32() + { + ReadInternal(_buffer, 4); + + return BinaryPrimitives.ReadInt32BigEndian(_buffer); + } + + /// + /// Reads a 64-bit signed integer from the stream. + /// 8 bytes are read. + /// + /// The 64-bit integer read. + public long ReadInt64() + { + ReadInternal(_buffer, 8); + + return BinaryPrimitives.ReadInt64BigEndian(_buffer); + } + + /// + /// Reads a 16-bit unsigned integer from the stream. + /// 2 bytes are read. + /// + /// The 16-bit unsigned integer read. + public ushort ReadUInt16() + { + ReadInternal(_buffer, 2); + + return BinaryPrimitives.ReadUInt16BigEndian(_buffer); + } + + /// + /// Reads a 16-bit unsigned integer from the stream representing an offset position. + /// 2 bytes are read. + /// + /// The 16-bit unsigned integer read. + public ushort ReadOffset16() => ReadUInt16(); + + public TEnum ReadUInt16() + where TEnum : struct, Enum + { + TryConvert(ReadUInt16(), out TEnum value); + return value; + } + + /// + /// Reads array of 16-bit unsigned integers from the stream. + /// + /// The length. + /// + /// The 16-bit unsigned integer read. + /// + public ushort[] ReadUInt16Array(int length) + { + ushort[] data = new ushort[length]; + for (int i = 0; i < length; i++) + { + data[i] = ReadUInt16(); + } + + return data; + } + + /// + /// Reads array of 16-bit unsigned integers from the stream to the buffer. + /// + /// The buffer to read to. + public void ReadUInt16Array(Span buffer) + { + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = ReadUInt16(); + } + } + + /// + /// Reads array or 32-bit unsigned integers from the stream. + /// + /// The length. + /// + /// The 32-bit unsigned integer read. + /// + public uint[] ReadUInt32Array(int length) + { + uint[] data = new uint[length]; + for (int i = 0; i < length; i++) + { + data[i] = ReadUInt32(); + } + + return data; + } + + public byte[] ReadUInt8Array(int length) + { + byte[] data = new byte[length]; + + ReadInternal(data, length); + + return data; + } + + /// + /// Reads array of 16-bit unsigned integers from the stream. + /// + /// The length. + /// + /// The 16-bit signed integer read. + /// + public short[] ReadInt16Array(int length) + { + short[] data = new short[length]; + for (int i = 0; i < length; i++) + { + data[i] = ReadInt16(); + } + + return data; + } + + /// + /// Reads an array of 16-bit signed integers from the stream to the buffer. + /// + /// The buffer to read to. + public void ReadInt16Array(Span buffer) + { + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = ReadInt16(); + } + } + + /// + /// Reads a 8-bit unsigned integer from the stream, using the bit converter + /// for this reader. 1 bytes are read. + /// + /// The 8-bit unsigned integer read. + public byte ReadUInt8() + { + ReadInternal(_buffer, 1); + return _buffer[0]; + } + + /// + /// Reads a 24-bit unsigned integer from the stream, using the bit converter + /// for this reader. 3 bytes are read. + /// + /// The 24-bit unsigned integer read. + public int ReadUInt24() + { + byte highByte = ReadByte(); + return (highByte << 16) | ReadUInt16(); + } + + /// + /// Reads a 32-bit unsigned integer from the stream, using the bit converter + /// for this reader. 4 bytes are read. + /// + /// The 32-bit unsigned integer read. + public uint ReadUInt32() + { + ReadInternal(_buffer, 4); + + return BinaryPrimitives.ReadUInt32BigEndian(_buffer); + } + + /// + /// Reads a 32-bit unsigned integer from the stream representing an offset position. + /// 4 bytes are read. + /// + /// The 32-bit unsigned integer read. + public uint ReadOffset32() => ReadUInt32(); + + /// + /// Reads the specified number of bytes, returning them in a new byte array. + /// If not enough bytes are available before the end of the stream, this + /// method will return what is available. + /// + /// The number of bytes to read. + /// The bytes read. + public byte[] ReadBytes(int count) + { + byte[] ret = new byte[count]; + int index = 0; + while (index < count) + { + int read = BaseStream.Read(ret, index, count - index); + + // Stream has finished half way through. That's fine, return what we've got. + if (read == 0) + { + byte[] copy = new byte[index]; + Buffer.BlockCopy(ret, 0, copy, 0, index); + return copy; + } + + index += read; + } + + return ret; + } + + /// + /// Reads a string of a specific length, which specifies the number of bytes + /// to read from the stream. These bytes are then converted into a string with + /// the encoding for this reader. + /// + /// The bytes to read. + /// The encoding. + /// + /// The string read from the stream. + /// + public string ReadString(int bytesToRead, Encoding encoding) + { + byte[] data = new byte[bytesToRead]; + ReadInternal(data, bytesToRead); + return encoding.GetString(data, 0, data.Length); + } + + /// + /// Reads the uint32 string. + /// + /// a 4 character long UTF8 encoded string. + public string ReadTag() + { + ReadInternal(_buffer, 4); + + return Encoding.UTF8.GetString(_buffer, 0, 4); + } + + /// + /// Reads an offset consuming the given nuber of bytes. + /// + /// The offset size in bytes. + /// The 32-bit signed integer representing the offset. + /// Size is not in range. + public int ReadOffset(int size) + => size switch + { + 1 => ReadByte(), + 2 => (ReadByte() << 8) | (ReadByte() << 0), + 3 => (ReadByte() << 16) | (ReadByte() << 8) | (ReadByte() << 0), + 4 => (ReadByte() << 24) | (ReadByte() << 16) | (ReadByte() << 8) | (ReadByte() << 0), + _ => throw new InvalidOperationException(), + }; + + /// + /// Reads the given number of bytes from the stream, throwing an exception + /// if they can't all be read. + /// + /// Buffer to read into. + /// Number of bytes to read. + private void ReadInternal(byte[] data, int size) + { + int index = 0; + + while (index < size) + { + int read = BaseStream.Read(data, index, size - index); + if (read == 0) + { + throw new EndOfStreamException($"End of stream reached with {size - index} byte{(size - index == 1 ? "s" : string.Empty)} left to read."); + } + + index += read; + } + } + + public void Dispose() + { + if (!_leaveOpen) + { + BaseStream?.Dispose(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryConvert(T input, out TEnum value) + where T : struct, IConvertible, IFormattable, IComparable + where TEnum : struct, Enum + { + if (Unsafe.SizeOf() == Unsafe.SizeOf()) + { + value = Unsafe.As(ref input); + return true; + } + + value = default; + return false; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/EncodingIDExtensions.cs b/src/Avalonia.Base/Media/Fonts/Tables/EncodingIDExtensions.cs new file mode 100644 index 0000000000..6c054648cd --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/EncodingIDExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +using System.Text; + +namespace Avalonia.Media.Fonts.Tables +{ + /// + /// Converts encoding ID to TextEncoding + /// + internal static class EncodingIDExtensions + { + /// + /// Converts encoding ID to TextEncoding + /// + /// The identifier. + /// the encoding for this encoding ID + public static Encoding AsEncoding(this EncodingIDs id) + { + switch (id) + { + case EncodingIDs.Unicode11: + case EncodingIDs.Unicode2: + return Encoding.BigEndianUnicode; + default: + return Encoding.UTF8; + } + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/EncodingIDs.cs b/src/Avalonia.Base/Media/Fonts/Tables/EncodingIDs.cs new file mode 100644 index 0000000000..78ab91cc77 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/EncodingIDs.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +namespace Avalonia.Media.Fonts.Tables +{ + /// + /// Encoding IDS + /// + internal enum EncodingIDs : ushort + { + /// + /// Unicode 1.0 semantics + /// + Unicode1 = 0, + + /// + /// Unicode 1.1 semantics + /// + Unicode11 = 1, + + /// + /// ISO/IEC 10646 semantics + /// + ISO10646 = 2, + + /// + /// Unicode 2.0 and onwards semantics, Unicode BMP only (cmap subtable formats 0, 4, 6). + /// + Unicode2 = 3, + + /// + /// Unicode 2.0 and onwards semantics, Unicode full repertoire (cmap subtable formats 0, 4, 6, 10, 12). + /// + Unicode2Plus = 4, + + /// + /// Unicode Variation Sequences (cmap subtable format 14). + /// + UnicodeVariationSequences = 5, + + /// + /// Unicode full repertoire (cmap subtable formats 0, 4, 6, 10, 12, 13) + /// + UnicodeFull = 6, + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs new file mode 100644 index 0000000000..0a916c7ed0 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +using System.Collections.Generic; +using System.IO; + +namespace Avalonia.Media.Fonts.Tables +{ + /// + /// Features provide information about how to use the glyphs in a font to render a script or language. + /// For example, an Arabic font might have a feature for substituting initial glyph forms, and a Kanji font + /// might have a feature for positioning glyphs vertically. All OpenType Layout features define data for + /// glyph substitution, glyph positioning, or both. + /// + /// + /// + internal class FeatureListTable + { + private static OpenTypeTag GSubTag = OpenTypeTag.Parse("GSUB"); + private static OpenTypeTag GPosTag = OpenTypeTag.Parse("GPOS"); + + private FeatureListTable(IReadOnlyList features) + { + Features = features; + } + + public IReadOnlyList Features { get; } + + public static FeatureListTable? LoadGSub(IGlyphTypeface glyphTypeface) + { + if (!glyphTypeface.TryGetTable(GSubTag, out var gPosTable)) + { + return null; + } + + using var stream = new MemoryStream(gPosTable); + using var reader = new BigEndianBinaryReader(stream, false); + + return Load(reader); + + } + public static FeatureListTable? LoadGPos(IGlyphTypeface glyphTypeface) + { + if (!glyphTypeface.TryGetTable(GPosTag, out var gSubTable)) + { + return null; + } + + using var stream = new MemoryStream(gSubTable); + using var reader = new BigEndianBinaryReader(stream, false); + + return Load(reader); + + } + + private static FeatureListTable Load(BigEndianBinaryReader reader) + { + // GPOS/GSUB Header, Version 1.0 + // +----------+-------------------+-----------------------------------------------------------+ + // | Type | Name | Description | + // +==========+===================+===========================================================+ + // | uint16 | majorVersion | Major version of the GPOS table, = 1 | + // +----------+-------------------+-----------------------------------------------------------+ + // | uint16 | minorVersion | Minor version of the GPOS table, = 0 | + // +----------+-------------------+-----------------------------------------------------------+ + // | Offset16 | scriptListOffset | Offset to ScriptList table, from beginning of GPOS table | + // +----------+-------------------+-----------------------------------------------------------+ + // | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GPOS table | + // +----------+-------------------+-----------------------------------------------------------+ + // | Offset16 | lookupListOffset | Offset to LookupList table, from beginning of GPOS table | + // +----------+-------------------+-----------------------------------------------------------+ + + reader.ReadUInt16(); + reader.ReadUInt16(); + + reader.ReadOffset16(); + var featureListOffset = reader.ReadOffset16(); + + return Load(reader, featureListOffset); + } + + private static FeatureListTable Load(BigEndianBinaryReader reader, long offset) + { + // FeatureList + // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +===============+==============================+=================================================================================================================+ + // | uint16 | featureCount | Number of FeatureRecords in this table | + // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+ + // | FeatureRecord | featureRecords[featureCount] | Array of FeatureRecords — zero-based (first feature has FeatureIndex = 0), listed alphabetically by feature tag | + // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+ + reader.Seek(offset, SeekOrigin.Begin); + + var featureCount = reader.ReadUInt16(); + + var features = new List(featureCount); + + for (var i = 0; i < featureCount; i++) + { + // FeatureRecord + // +----------+---------------+--------------------------------------------------------+ + // | Type | Name | Description | + // +==========+===============+========================================================+ + // | Tag | featureTag | 4-byte feature identification tag | + // +----------+---------------+--------------------------------------------------------+ + // | Offset16 | featureOffset | Offset to Feature table, from beginning of FeatureList | + // +----------+---------------+--------------------------------------------------------+ + var featureTag = reader.ReadUInt32(); + + reader.ReadOffset16(); + + var tag = new OpenTypeTag(featureTag); + + if (!features.Contains(tag)) + { + features.Add(tag); + } + } + + return new FeatureListTable(features /*featureTables*/); + } + + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs new file mode 100644 index 0000000000..0942296536 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs @@ -0,0 +1,153 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +using System.IO; + +namespace Avalonia.Media.Fonts.Tables +{ + internal class HorizontalHeadTable + { + internal const string TableName = "hhea"; + internal static OpenTypeTag Tag = OpenTypeTag.Parse(TableName); + + public HorizontalHeadTable( + short ascender, + short descender, + short lineGap, + ushort advanceWidthMax, + short minLeftSideBearing, + short minRightSideBearing, + short xMaxExtent, + short caretSlopeRise, + short caretSlopeRun, + short caretOffset, + ushort numberOfHMetrics) + { + Ascender = ascender; + Descender = descender; + LineGap = lineGap; + AdvanceWidthMax = advanceWidthMax; + MinLeftSideBearing = minLeftSideBearing; + MinRightSideBearing = minRightSideBearing; + XMaxExtent = xMaxExtent; + CaretSlopeRise = caretSlopeRise; + CaretSlopeRun = caretSlopeRun; + CaretOffset = caretOffset; + NumberOfHMetrics = numberOfHMetrics; + } + + public ushort AdvanceWidthMax { get; } + + public short Ascender { get; } + + public short CaretOffset { get; } + + public short CaretSlopeRise { get; } + + public short CaretSlopeRun { get; } + + public short Descender { get; } + + public short LineGap { get; } + + public short MinLeftSideBearing { get; } + + public short MinRightSideBearing { get; } + + public ushort NumberOfHMetrics { get; } + + public short XMaxExtent { get; } + + public static HorizontalHeadTable? Load(IGlyphTypeface glyphTypeface) + { + if (!glyphTypeface.TryGetTable(Tag, out var table)) + { + return null; + } + + using var stream = new MemoryStream(table); + using var binaryReader = new BigEndianBinaryReader(stream, false); + + // Move to start of table. + return Load(binaryReader); + } + + public static HorizontalHeadTable Load(BigEndianBinaryReader reader) + { + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | Type | Name | Description | + // +========+=====================+=================================================================================+ + // | Fixed | version | 0x00010000 (1.0) | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | ascent | Distance from baseline of highest ascender | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | descent | Distance from baseline of lowest descender | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | lineGap | typographic line gap | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | uFWord | advanceWidthMax | must be consistent with horizontal metrics | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | minLeftSideBearing | must be consistent with horizontal metrics | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | minRightSideBearing | must be consistent with horizontal metrics | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | xMaxExtent | max(lsb + (xMax-xMin)) | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | caretSlopeRise | used to calculate the slope of the caret (rise/run) set to 1 for vertical caret | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | caretSlopeRun | 0 for vertical | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | FWord | caretOffset | set value to 0 for non-slanted fonts | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | reserved | set value to 0 | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | reserved | set value to 0 | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | reserved | set value to 0 | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | reserved | set value to 0 | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | int16 | metricDataFormat | 0 for current format | + // +--------+---------------------+---------------------------------------------------------------------------------+ + // | uint16 | numOfLongHorMetrics | number of advance widths in metrics table | + // +--------+---------------------+---------------------------------------------------------------------------------+ + ushort majorVersion = reader.ReadUInt16(); + ushort minorVersion = reader.ReadUInt16(); + short ascender = reader.ReadFWORD(); + short descender = reader.ReadFWORD(); + short lineGap = reader.ReadFWORD(); + ushort advanceWidthMax = reader.ReadUFWORD(); + short minLeftSideBearing = reader.ReadFWORD(); + short minRightSideBearing = reader.ReadFWORD(); + short xMaxExtent = reader.ReadFWORD(); + short caretSlopeRise = reader.ReadInt16(); + short caretSlopeRun = reader.ReadInt16(); + short caretOffset = reader.ReadInt16(); + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + reader.ReadInt16(); // reserved + short metricDataFormat = reader.ReadInt16(); // 0 + if (metricDataFormat != 0) + { + throw new InvalidFontTableException($"Expected metricDataFormat = 0 found {metricDataFormat}", TableName); + } + + ushort numberOfHMetrics = reader.ReadUInt16(); + + return new HorizontalHeadTable( + ascender, + descender, + lineGap, + advanceWidthMax, + minLeftSideBearing, + minRightSideBearing, + xMaxExtent, + caretSlopeRise, + caretSlopeRun, + caretOffset, + numberOfHMetrics); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/InvalidFontTableException.cs b/src/Avalonia.Base/Media/Fonts/Tables/InvalidFontTableException.cs new file mode 100644 index 0000000000..d8be26a848 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/InvalidFontTableException.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +using System; + +namespace Avalonia.Media.Fonts.Tables +{ + /// + /// Exception font loading can throw if it encounters invalid data during font loading. + /// + /// + public class InvalidFontTableException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The table. + public InvalidFontTableException(string message, string table) + : base(message) + => Table = table; + + /// + /// Gets the table where the error originated. + /// + public string Table { get; } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/KnownNameIds.cs b/src/Avalonia.Base/Media/Fonts/Tables/KnownNameIds.cs new file mode 100644 index 0000000000..82e6926600 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/KnownNameIds.cs @@ -0,0 +1,123 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +namespace Avalonia.Media.Fonts.Tables +{ + /// + /// Provides enumeration of common name ids + /// + /// + public enum KnownNameIds : ushort + { + /// + /// The copyright notice + /// + CopyrightNotice = 0, + + /// + /// The font family name; Up to four fonts can share the Font Family name, forming a font style linking + /// group (regular, italic, bold, bold italic — as defined by OS/2.fsSelection bit settings). + /// + FontFamilyName = 1, + + /// + /// The font subfamily name; The Font Subfamily name distinguishes the font in a group with the same Font Family name (name ID 1). + /// This is assumed to address style (italic, oblique) and weight (light, bold, black, etc.). A font with no particular differences + /// in weight or style (e.g. medium weight, not italic and fsSelection bit 6 set) should have the string "Regular" stored in this position. + /// + FontSubfamilyName = 2, + + /// + /// The unique font identifier + /// + UniqueFontID = 3, + + /// + /// The full font name; a combination of strings 1 and 2, or a similar human-readable variant. If string 2 is "Regular", it is sometimes omitted from name ID 4. + /// + FullFontName = 4, + + /// + /// Version string. Should begin with the syntax 'Version <number>.<number>' (upper case, lower case, or mixed, with a space between "Version" and the number). + /// The string must contain a version number of the following form: one or more digits (0-9) of value less than 65,535, followed by a period, followed by one or more + /// digits of value less than 65,535. Any character other than a digit will terminate the minor number. A character such as ";" is helpful to separate different pieces of version information. + /// The first such match in the string can be used by installation software to compare font versions. + /// Note that some installers may require the string to start with "Version ", followed by a version number as above. + /// + Version = 5, + + /// + /// Postscript name for the font; Name ID 6 specifies a string which is used to invoke a PostScript language font that corresponds to this OpenType font. + /// When translated to ASCII, the name string must be no longer than 63 characters and restricted to the printable ASCII subset, codes 33 to 126, + /// except for the 10 characters '[', ']', '(', ')', '{', '}', '<', '>', '/', '%'. + /// In a CFF OpenType font, there is no requirement that this name be the same as the font name in the CFF’s Name INDEX. + /// Thus, the same CFF may be shared among multiple font components in a Font Collection. See the 'name' table section of + /// Recommendations for OpenType fonts "" for additional information. + /// + PostscriptName = 6, + + /// + /// Trademark; this is used to save any trademark notice/information for this font. Such information should + /// be based on legal advice. This is distinctly separate from the copyright. + /// + Trademark = 7, + + /// + /// The manufacturer + /// + Manufacturer = 8, + + /// + /// Designer; name of the designer of the typeface. + /// + Designer = 9, + + /// + /// Description; description of the typeface. Can contain revision information, usage recommendations, history, features, etc. + /// + Description = 10, + + /// + /// URL Vendor; URL of font vendor (with protocol, e.g., http://, ftp://). If a unique serial number is embedded in + /// the URL, it can be used to register the font. + /// + VendorUrl = 11, + + /// + /// URL Designer; URL of typeface designer (with protocol, e.g., http://, ftp://). + /// + DesignerUrl = 12, + + /// + /// License Description; description of how the font may be legally used, or different example scenarios for licensed use. + /// This field should be written in plain language, not legalese. + /// + LicenseDescription = 13, + + /// + /// License Info URL; URL where additional licensing information can be found. + /// + LicenseInfoUrl = 14, + + /// + /// Typographic Family name: The typographic family grouping doesn't impose any constraints on the number of faces within it, + /// in contrast with the 4-style family grouping (ID 1), which is present both for historical reasons and to express style linking groups. + /// If name ID 16 is absent, then name ID 1 is considered to be the typographic family name. + /// (In earlier versions of the specification, name ID 16 was known as "Preferred Family".) + /// + TypographicFamilyName = 16, + + /// + /// Typographic Subfamily name: This allows font designers to specify a subfamily name within the typographic family grouping. + /// This string must be unique within a particular typographic family. If it is absent, then name ID 2 is considered to be the + /// typographic subfamily name. (In earlier versions of the specification, name ID 17 was known as "Preferred Subfamily".) + /// + TypographicSubfamilyName = 17, + + /// + /// Sample text; This can be the font name, or any other text that the designer thinks is the best sample to display the font in. + /// + SampleText = 19, + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/MissingFontTableException.cs b/src/Avalonia.Base/Media/Fonts/Tables/MissingFontTableException.cs new file mode 100644 index 0000000000..890414fc59 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/MissingFontTableException.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +using System; + +namespace Avalonia.Media.Fonts.Tables +{ + /// + /// Exception font loading can throw if it finds a required table is missing during font loading. + /// + /// + public class MissingFontTableException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The table. + public MissingFontTableException(string message, string table) + : base(message) + => Table = table; + + /// + /// Gets the table where the error originated. + /// + public string Table { get; } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs new file mode 100644 index 0000000000..7a7ad71995 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +namespace Avalonia.Media.Fonts.Tables.Name +{ + internal class NameRecord + { + private readonly string value; + + public NameRecord(PlatformIDs platform, ushort languageId, KnownNameIds nameId, string value) + { + Platform = platform; + LanguageID = languageId; + NameID = nameId; + this.value = value; + } + + public PlatformIDs Platform { get; } + + public ushort LanguageID { get; } + + public KnownNameIds NameID { get; } + + internal StringLoader? StringReader { get; private set; } + + public string Value => StringReader?.Value ?? value; + + public static NameRecord Read(BigEndianBinaryReader reader) + { + var platform = reader.ReadUInt16(); + var encodingId = reader.ReadUInt16(); + var encoding = encodingId.AsEncoding(); + var languageID = reader.ReadUInt16(); + var nameID = reader.ReadUInt16(); + + var stringReader = StringLoader.Create(reader, encoding); + + return new NameRecord(platform, languageID, nameID, string.Empty) + { + StringReader = stringReader + }; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs new file mode 100644 index 0000000000..e2a5cbb681 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs @@ -0,0 +1,179 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +using System.Collections.Generic; +using System.IO; + +namespace Avalonia.Media.Fonts.Tables.Name +{ + internal class NameTable + { + internal const string TableName = "name"; + internal static readonly OpenTypeTag Tag = OpenTypeTag.Parse(TableName); + + private readonly NameRecord[] _names; + + internal NameTable(NameRecord[] names, IReadOnlyList languages) + { + _names = names; + Languages = languages; + } + + public IReadOnlyList Languages { get; } + + /// + /// Gets the name of the font. + /// + /// + /// The name of the font. + /// + public string Id(ushort culture) + => GetNameById(culture, KnownNameIds.UniqueFontID); + + /// + /// Gets the name of the font. + /// + /// + /// The name of the font. + /// + public string FontName(ushort culture) + => GetNameById(culture, KnownNameIds.FullFontName); + + /// + /// Gets the name of the font. + /// + /// + /// The name of the font. + /// + public string FontFamilyName(ushort culture) + => GetNameById(culture, KnownNameIds.FontFamilyName); + + /// + /// Gets the name of the font. + /// + /// + /// The name of the font. + /// + public string FontSubFamilyName(ushort culture) + => GetNameById(culture, KnownNameIds.FontSubfamilyName); + + public string GetNameById(ushort culture, KnownNameIds nameId) + { + var languageId = culture; + NameRecord? usaVersion = null; + NameRecord? firstWindows = null; + NameRecord? first = null; + foreach (var name in _names) + { + if (name.NameID == nameId) + { + // Get just the first one, just in case. + first ??= name; + if (name.Platform == PlatformIDs.Windows) + { + // If us not found return the first windows one. + firstWindows ??= name; + if (name.LanguageID == 0x0409) + { + // Grab the us version as its on next best match. + usaVersion ??= name; + } + + if (name.LanguageID == languageId) + { + // Return the most exact first. + return name.Value; + } + } + } + } + + return usaVersion?.Value ?? + firstWindows?.Value ?? + first?.Value ?? + string.Empty; + } + + public string GetNameById(ushort culture, ushort nameId) + => GetNameById(culture, (KnownNameIds)nameId); + + public static NameTable Load(IGlyphTypeface glyphTypeface) + { + if (!glyphTypeface.TryGetTable(Tag, out var table)) + { + throw new MissingFontTableException("Could not load table", "name"); + } + + using var stream = new MemoryStream(table); + using var binaryReader = new BigEndianBinaryReader(stream, false); + + // Move to start of table. + return Load(binaryReader); + } + + public static NameTable Load(BigEndianBinaryReader reader) + { + var strings = new List(); + var format = reader.ReadUInt16(); + var nameCount = reader.ReadUInt16(); + var stringOffset = reader.ReadUInt16(); + + var names = new NameRecord[nameCount]; + + for (var i = 0; i < nameCount; i++) + { + names[i] = NameRecord.Read(reader); + + var sr = names[i].StringReader; + + if (sr is not null) + { + strings.Add(sr); + } + } + + //var languageNames = Array.Empty(); + + //if (format == 1) + //{ + // // Format 1 adds language data. + // var langCount = reader.ReadUInt16(); + // languageNames = new StringLoader[langCount]; + + // for (var i = 0; i < langCount; i++) + // { + // languageNames[i] = StringLoader.Create(reader); + + // strings.Add(languageNames[i]); + // } + //} + + foreach (var readable in strings) + { + var readableStartOffset = stringOffset + readable.Offset; + + reader.Seek(readableStartOffset, SeekOrigin.Begin); + + readable.LoadValue(reader); + } + + var cultures = new List(); + + foreach (var nameRecord in names) + { + if (nameRecord.NameID != KnownNameIds.FontFamilyName || nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0) + { + continue; + } + + if (!cultures.Contains(nameRecord.LanguageID)) + { + cultures.Add(nameRecord.LanguageID); + } + } + + return new NameTable(names, cultures); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs b/src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs new file mode 100644 index 0000000000..73d80edd7d --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs @@ -0,0 +1,423 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +using System; +using System.IO; + +namespace Avalonia.Media.Fonts.Tables +{ + internal sealed class OS2Table + { + internal const string TableName = "OS/2"; + internal static OpenTypeTag Tag = OpenTypeTag.Parse(TableName); + + private readonly ushort styleType; + private readonly byte[] panose; + private readonly short capHeight; + private readonly short familyClass; + private readonly short heightX; + private readonly string tag; + private readonly ushort codePageRange1; + private readonly ushort codePageRange2; + private readonly uint unicodeRange1; + private readonly uint unicodeRange2; + private readonly uint unicodeRange3; + private readonly uint unicodeRange4; + private readonly ushort breakChar; + private readonly ushort defaultChar; + private readonly ushort firstCharIndex; + private readonly ushort lastCharIndex; + private readonly ushort lowerOpticalPointSize; + private readonly ushort maxContext; + private readonly ushort upperOpticalPointSize; + private readonly ushort weightClass; + private readonly ushort widthClass; + private readonly short averageCharWidth; + + public OS2Table( + short averageCharWidth, + ushort weightClass, + ushort widthClass, + ushort styleType, + short subscriptXSize, + short subscriptYSize, + short subscriptXOffset, + short subscriptYOffset, + short superscriptXSize, + short superscriptYSize, + short superscriptXOffset, + short superscriptYOffset, + short strikeoutSize, + short strikeoutPosition, + short familyClass, + byte[] panose, + uint unicodeRange1, + uint unicodeRange2, + uint unicodeRange3, + uint unicodeRange4, + string tag, + FontStyleSelection fontStyle, + ushort firstCharIndex, + ushort lastCharIndex, + short typoAscender, + short typoDescender, + short typoLineGap, + ushort winAscent, + ushort winDescent) + { + this.averageCharWidth = averageCharWidth; + this.weightClass = weightClass; + this.widthClass = widthClass; + this.styleType = styleType; + SubscriptXSize = subscriptXSize; + SubscriptYSize = subscriptYSize; + SubscriptXOffset = subscriptXOffset; + SubscriptYOffset = subscriptYOffset; + SuperscriptXSize = superscriptXSize; + SuperscriptYSize = superscriptYSize; + SuperscriptXOffset = superscriptXOffset; + SuperscriptYOffset = superscriptYOffset; + StrikeoutSize = strikeoutSize; + StrikeoutPosition = strikeoutPosition; + this.familyClass = familyClass; + this.panose = panose; + this.unicodeRange1 = unicodeRange1; + this.unicodeRange2 = unicodeRange2; + this.unicodeRange3 = unicodeRange3; + this.unicodeRange4 = unicodeRange4; + this.tag = tag; + FontStyle = fontStyle; + this.firstCharIndex = firstCharIndex; + this.lastCharIndex = lastCharIndex; + TypoAscender = typoAscender; + TypoDescender = typoDescender; + TypoLineGap = typoLineGap; + WinAscent = winAscent; + WinDescent = winDescent; + } + + public OS2Table( + OS2Table version0Table, + ushort codePageRange1, + ushort codePageRange2, + short heightX, + short capHeight, + ushort defaultChar, + ushort breakChar, + ushort maxContext) + : this( + version0Table.averageCharWidth, + version0Table.weightClass, + version0Table.widthClass, + version0Table.styleType, + version0Table.SubscriptXSize, + version0Table.SubscriptYSize, + version0Table.SubscriptXOffset, + version0Table.SubscriptYOffset, + version0Table.SuperscriptXSize, + version0Table.SuperscriptYSize, + version0Table.SuperscriptXOffset, + version0Table.SuperscriptYOffset, + version0Table.StrikeoutSize, + version0Table.StrikeoutPosition, + version0Table.familyClass, + version0Table.panose, + version0Table.unicodeRange1, + version0Table.unicodeRange2, + version0Table.unicodeRange3, + version0Table.unicodeRange4, + version0Table.tag, + version0Table.FontStyle, + version0Table.firstCharIndex, + version0Table.lastCharIndex, + version0Table.TypoAscender, + version0Table.TypoDescender, + version0Table.TypoLineGap, + version0Table.WinAscent, + version0Table.WinDescent) + { + this.codePageRange1 = codePageRange1; + this.codePageRange2 = codePageRange2; + this.heightX = heightX; + this.capHeight = capHeight; + this.defaultChar = defaultChar; + this.breakChar = breakChar; + this.maxContext = maxContext; + } + + public OS2Table(OS2Table versionLessThan5Table, ushort lowerOpticalPointSize, ushort upperOpticalPointSize) + : this( + versionLessThan5Table, + versionLessThan5Table.codePageRange1, + versionLessThan5Table.codePageRange2, + versionLessThan5Table.heightX, + versionLessThan5Table.capHeight, + versionLessThan5Table.defaultChar, + versionLessThan5Table.breakChar, + versionLessThan5Table.maxContext) + { + this.lowerOpticalPointSize = lowerOpticalPointSize; + this.upperOpticalPointSize = upperOpticalPointSize; + } + + [Flags] + internal enum FontStyleSelection : ushort + { + /// + /// Font contains italic or oblique characters, otherwise they are upright. + /// + ITALIC = 1, + + /// + /// Characters are underscored. + /// + UNDERSCORE = 1 << 1, + + /// + /// Characters have their foreground and background reversed. + /// + NEGATIVE = 1 << 2, + + /// + /// characters, otherwise they are solid. + /// + OUTLINED = 1 << 3, + + /// + /// Characters are overstruck. + /// + STRIKEOUT = 1 << 4, + + /// + /// Characters are emboldened. + /// + BOLD = 1 << 5, + + /// + /// Characters are in the standard weight/style for the font. + /// + REGULAR = 1 << 6, + + /// + /// If set, it is strongly recommended to use OS/2.typoAscender - OS/2.typoDescender+ OS/2.typoLineGap as a value for default line spacing for this font. + /// + USE_TYPO_METRICS = 1 << 7, + + /// + /// The font has ‘name’ table strings consistent with a weight/width/slope family without requiring use of ‘name’ IDs 21 and 22. (Please see more detailed description below.) + /// + WWS = 1 << 8, + + /// + /// Font contains oblique characters. + /// + OBLIQUE = 1 << 9, + + // 10–15 Reserved; set to 0. + } + + public FontStyleSelection FontStyle { get; } + + public short TypoAscender { get; } + + public short TypoDescender { get; } + + public short TypoLineGap { get; } + + public ushort WinAscent { get; } + + public ushort WinDescent { get; } + + public short StrikeoutPosition { get; } + + public short StrikeoutSize { get; } + + public short SubscriptXOffset { get; } + + public short SubscriptXSize { get; } + + public short SubscriptYOffset { get; } + + public short SubscriptYSize { get; } + + public short SuperscriptXOffset { get; } + + public short SuperscriptXSize { get; } + + public short SuperscriptYOffset { get; } + + public short SuperscriptYSize { get; } + + public static OS2Table? Load(IGlyphTypeface glyphTypeface) + { + if (!glyphTypeface.TryGetTable(Tag, out var table)) + { + return null; + } + + using var stream = new MemoryStream(table); + using var binaryReader = new BigEndianBinaryReader(stream, false); + + // Move to start of table. + return Load(binaryReader); + } + + public static OS2Table Load(BigEndianBinaryReader reader) + { + // Version 1.0 + // Type | Name | Comments + // -------|------------------------|----------------------- + // uint16 |version | 0x0005 + // int16 |xAvgCharWidth | + // uint16 |usWeightClass | + // uint16 |usWidthClass | + // uint16 |fsType | + // int16 |ySubscriptXSize | + // int16 |ySubscriptYSize | + // int16 |ySubscriptXOffset | + // int16 |ySubscriptYOffset | + // int16 |ySuperscriptXSize | + // int16 |ySuperscriptYSize | + // int16 |ySuperscriptXOffset | + // int16 |ySuperscriptYOffset | + // int16 |yStrikeoutSize | + // int16 |yStrikeoutPosition | + // int16 |sFamilyClass | + // uint8 |panose[10] | + // uint32 |ulUnicodeRange1 | Bits 0–31 + // uint32 |ulUnicodeRange2 | Bits 32–63 + // uint32 |ulUnicodeRange3 | Bits 64–95 + // uint32 |ulUnicodeRange4 | Bits 96–127 + // Tag |achVendID | + // uint16 |fsSelection | + // uint16 |usFirstCharIndex | + // uint16 |usLastCharIndex | + // int16 |sTypoAscender | + // int16 |sTypoDescender | + // int16 |sTypoLineGap | + // uint16 |usWinAscent | + // uint16 |usWinDescent | + // uint32 |ulCodePageRange1 | Bits 0–31 + // uint32 |ulCodePageRange2 | Bits 32–63 + // int16 |sxHeight | + // int16 |sCapHeight | + // uint16 |usDefaultChar | + // uint16 |usBreakChar | + // uint16 |usMaxContext | + // uint16 |usLowerOpticalPointSize | + // uint16 |usUpperOpticalPointSize | + ushort version = reader.ReadUInt16(); // assert 0x0005 + short averageCharWidth = reader.ReadInt16(); + ushort weightClass = reader.ReadUInt16(); + ushort widthClass = reader.ReadUInt16(); + ushort styleType = reader.ReadUInt16(); + short subscriptXSize = reader.ReadInt16(); + short subscriptYSize = reader.ReadInt16(); + short subscriptXOffset = reader.ReadInt16(); + short subscriptYOffset = reader.ReadInt16(); + + short superscriptXSize = reader.ReadInt16(); + short superscriptYSize = reader.ReadInt16(); + short superscriptXOffset = reader.ReadInt16(); + short superscriptYOffset = reader.ReadInt16(); + + short strikeoutSize = reader.ReadInt16(); + short strikeoutPosition = reader.ReadInt16(); + short familyClass = reader.ReadInt16(); + byte[] panose = reader.ReadUInt8Array(10); + uint unicodeRange1 = reader.ReadUInt32(); // Bits 0–31 + uint unicodeRange2 = reader.ReadUInt32(); // Bits 32–63 + uint unicodeRange3 = reader.ReadUInt32(); // Bits 64–95 + uint unicodeRange4 = reader.ReadUInt32(); // Bits 96–127 + string tag = reader.ReadTag(); + FontStyleSelection fontStyle = reader.ReadUInt16(); + ushort firstCharIndex = reader.ReadUInt16(); + ushort lastCharIndex = reader.ReadUInt16(); + short typoAscender = reader.ReadInt16(); + short typoDescender = reader.ReadInt16(); + short typoLineGap = reader.ReadInt16(); + ushort winAscent = reader.ReadUInt16(); + ushort winDescent = reader.ReadUInt16(); + + var version0Table = new OS2Table( + averageCharWidth, + weightClass, + widthClass, + styleType, + subscriptXSize, + subscriptYSize, + subscriptXOffset, + subscriptYOffset, + superscriptXSize, + superscriptYSize, + superscriptXOffset, + superscriptYOffset, + strikeoutSize, + strikeoutPosition, + familyClass, + panose, + unicodeRange1, + unicodeRange2, + unicodeRange3, + unicodeRange4, + tag, + fontStyle, + firstCharIndex, + lastCharIndex, + typoAscender, + typoDescender, + typoLineGap, + winAscent, + winDescent); + + if (version == 0) + { + return version0Table; + } + + short heightX = 0; + short capHeight = 0; + + ushort defaultChar = 0; + ushort breakChar = 0; + ushort maxContext = 0; + + ushort codePageRange1 = reader.ReadUInt16(); // Bits 0–31 + ushort codePageRange2 = reader.ReadUInt16(); // Bits 32–63 + + // fields exist only in > v1 https://docs.microsoft.com/en-us/typography/opentype/spec/os2 + if (version > 1) + { + heightX = reader.ReadInt16(); + capHeight = reader.ReadInt16(); + defaultChar = reader.ReadUInt16(); + breakChar = reader.ReadUInt16(); + maxContext = reader.ReadUInt16(); + } + + var versionLessThan5Table = new OS2Table( + version0Table, + codePageRange1, + codePageRange2, + heightX, + capHeight, + defaultChar, + breakChar, + maxContext); + + if (version < 5) + { + return versionLessThan5Table; + } + + ushort lowerOpticalPointSize = reader.ReadUInt16(); + ushort upperOpticalPointSize = reader.ReadUInt16(); + + return new OS2Table( + versionLessThan5Table, + lowerOpticalPointSize, + upperOpticalPointSize); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs b/src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs new file mode 100644 index 0000000000..c57c4e2726 --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +namespace Avalonia.Media.Fonts.Tables +{ + /// + /// platforms ids + /// + internal enum PlatformIDs : ushort + { + /// + /// Unicode platform + /// + Unicode = 0, + + /// + /// Script manager code + /// + Macintosh = 1, + + /// + /// [deprecated] ISO encoding + /// + ISO = 2, + + /// + /// Window encoding + /// + Windows = 3, + + /// + /// Custom platform + /// + Custom = 4 // Custom None + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs b/src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs new file mode 100644 index 0000000000..a42c87b5bd --- /dev/null +++ b/src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. +// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts + +using System.Diagnostics; +using System.Text; + +namespace Avalonia.Media.Fonts.Tables +{ + [DebuggerDisplay("Offset: {Offset}, Length: {Length}, Value: {Value}")] + internal class StringLoader + { + public StringLoader(ushort length, ushort offset, Encoding encoding) + { + Length = length; + Offset = offset; + Encoding = encoding; + Value = string.Empty; + } + + public ushort Length { get; } + + public ushort Offset { get; } + + public string Value { get; private set; } + + public Encoding Encoding { get; } + + public static StringLoader Create(BigEndianBinaryReader reader) + => Create(reader, Encoding.BigEndianUnicode); + + public static StringLoader Create(BigEndianBinaryReader reader, Encoding encoding) + => new StringLoader(reader.ReadUInt16(), reader.ReadUInt16(), encoding); + + public void LoadValue(BigEndianBinaryReader reader) + => Value = reader.ReadString(Length, Encoding).Replace("\0", string.Empty); + } +} diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index a751ccc9bc..a81dbdb3f0 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -153,7 +153,8 @@ namespace Avalonia.Media /// /// Gets the conservative bounding box of the . /// - public Rect Bounds => new Rect(new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height)); + public Rect Bounds => new Rect(new Point(BaselineOrigin.X, 0), + new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height)); public Rect InkBounds => PlatformImpl.Item.Bounds; @@ -573,7 +574,10 @@ namespace Avalonia.Media } } - nextCluster = _glyphInfos[currentIndex].GlyphCluster; + if (_glyphInfos.Count > 0 && currentIndex <= _glyphInfos.Count) + { + nextCluster = _glyphInfos[currentIndex].GlyphCluster; + } } var clusterLength = Math.Max(0, nextCluster - cluster); @@ -638,7 +642,7 @@ namespace Avalonia.Media { int firstCluster, lastCluster; - if (Characters.IsEmpty) + if (Characters.IsEmpty || _glyphInfos.Count == 0) { firstCluster = 0; lastCluster = 0; @@ -687,7 +691,7 @@ namespace Avalonia.Media return new GlyphRunMetrics { - Baseline = -GlyphTypeface.Metrics.Ascent * Scale, + Baseline = (-GlyphTypeface.Metrics.Ascent + GlyphTypeface.Metrics.LineGap) * Scale, Width = width, WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace, Height = height, diff --git a/src/Avalonia.Base/Media/IGlyphTypeface2.cs b/src/Avalonia.Base/Media/IGlyphTypeface2.cs index d0152ccb3c..2c7ea58bcb 100644 --- a/src/Avalonia.Base/Media/IGlyphTypeface2.cs +++ b/src/Avalonia.Base/Media/IGlyphTypeface2.cs @@ -1,16 +1,28 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; +using Avalonia.Media.Fonts; namespace Avalonia.Media { internal interface IGlyphTypeface2 : IGlyphTypeface { - /// /// Returns the font file stream represented by the object. /// /// The stream. /// Returns true if the stream can be obtained, otherwise false. bool TryGetStream([NotNullWhen(true)] out Stream? stream); + + /// + /// Gets the localized family names. + /// Keys are culture identifiers. + /// + IReadOnlyDictionary FamilyNames { get; } + + /// + /// Gets supported font features. + /// + IReadOnlyList SupportedFeatures { get; } } } diff --git a/src/Avalonia.Base/Media/LineSegment.cs b/src/Avalonia.Base/Media/LineSegment.cs index 68193bb770..8b0014fcc8 100644 --- a/src/Avalonia.Base/Media/LineSegment.cs +++ b/src/Avalonia.Base/Media/LineSegment.cs @@ -24,7 +24,7 @@ namespace Avalonia.Media internal override void ApplyTo(StreamGeometryContext ctx) { - ctx.LineTo(Point); + ctx.LineTo(Point, IsStroked); } public override string ToString() diff --git a/src/Avalonia.Base/Media/PathSegment.cs b/src/Avalonia.Base/Media/PathSegment.cs index 0b517e56f3..6b5f5f601d 100644 --- a/src/Avalonia.Base/Media/PathSegment.cs +++ b/src/Avalonia.Base/Media/PathSegment.cs @@ -3,5 +3,14 @@ namespace Avalonia.Media public abstract class PathSegment : AvaloniaObject { internal abstract void ApplyTo(StreamGeometryContext ctx); + + public static readonly StyledProperty IsStrokedProperty = + AvaloniaProperty.Register(nameof(IsStroked), true); + + public bool IsStroked + { + get => GetValue(IsStrokedProperty); + set => SetValue(IsStrokedProperty, value); + } } } diff --git a/src/Avalonia.Base/Media/PolyBezierSegment.cs b/src/Avalonia.Base/Media/PolyBezierSegment.cs new file mode 100644 index 0000000000..e77efe4b6d --- /dev/null +++ b/src/Avalonia.Base/Media/PolyBezierSegment.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using Avalonia.Utilities; + +namespace Avalonia.Media; + +/// +/// PolyBezierSegment +/// +public sealed class PolyBezierSegment : PathSegment +{ + /// + /// Points DirectProperty definition + /// + public static readonly DirectProperty PointsProperty = + AvaloniaProperty.RegisterDirect(nameof(Points), + o => o.Points, + (o, v) => o.Points = v); + + private Points? _points = []; + + public PolyBezierSegment() + { + + } + + public PolyBezierSegment(IEnumerable points, bool isStroked) + { + if (points is null) + { + throw new ArgumentNullException(nameof(points)); + } + + Points = new Points(points); + IsStroked = isStroked; + } + + /// + /// Gets or sets the Point collection that defines this object. + /// + /// + /// The points. + /// + [Metadata.Content] + public Points? Points + { + get => _points; + set => SetAndRaise(PointsProperty, ref _points, value); + } + + internal override void ApplyTo(StreamGeometryContext ctx) + { + var isStroken = this.IsStroked; + if (_points is { Count: > 0 } points) + { + var i = 0; + for (; i < points.Count; i += 3) + { + ctx.CubicBezierTo(points[i], + points[i + 1], + points[i + 2], + isStroken); + } + var delta = i - points.Count; + if (delta != 0) + { + Logging.Logger.TryGet(Logging.LogEventLevel.Warning, + Logging.LogArea.Visual) + ?.Log(nameof(PolyBezierSegment), + $"{nameof(PolyBezierSegment)} has ivalid number of points. Last {Math.Abs(delta)} points will be ignored."); + } + } + } + + public override string ToString() + { + var builder = StringBuilderCache.Acquire(); + if (_points is { Count: > 0 } points) + { + builder.Append('C').Append(' '); + foreach (var point in _points) + { + builder.Append(FormattableString.Invariant($"{point}")); + builder.Append(' '); + } + builder.Length = builder.Length - 1; + } + return StringBuilderCache.GetStringAndRelease(builder); + } +} diff --git a/src/Avalonia.Base/Media/PolyLineSegment.cs b/src/Avalonia.Base/Media/PolyLineSegment.cs index da87a2155b..c97c016ab3 100644 --- a/src/Avalonia.Base/Media/PolyLineSegment.cs +++ b/src/Avalonia.Base/Media/PolyLineSegment.cs @@ -53,7 +53,7 @@ namespace Avalonia.Media { for (int i = 0; i < points.Count; i++) { - ctx.LineTo(points[i]); + ctx.LineTo(points[i], IsStroked); } } } diff --git a/src/Avalonia.Base/Media/QuadraticBezierSegment .cs b/src/Avalonia.Base/Media/QuadraticBezierSegment .cs index 01d22f2043..a338080065 100644 --- a/src/Avalonia.Base/Media/QuadraticBezierSegment .cs +++ b/src/Avalonia.Base/Media/QuadraticBezierSegment .cs @@ -42,7 +42,7 @@ namespace Avalonia.Media internal override void ApplyTo(StreamGeometryContext ctx) { - ctx.QuadraticBezierTo(Point1, Point2); + ctx.QuadraticBezierTo(Point1, Point2, IsStroked); } public override string ToString() diff --git a/src/Avalonia.Base/Media/StreamGeometryContext.cs b/src/Avalonia.Base/Media/StreamGeometryContext.cs index 66fb65a6c2..c8072564b1 100644 --- a/src/Avalonia.Base/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Base/Media/StreamGeometryContext.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media /// of is obtained by calling /// . /// - public class StreamGeometryContext : IGeometryContext + public class StreamGeometryContext : IGeometryContext, IGeometryContext2 { private readonly IStreamGeometryContextImpl _impl; @@ -102,5 +102,46 @@ namespace Avalonia.Media { _impl.Dispose(); } + + /// + public void LineTo(Point point, bool isStroked) + { + if (_impl is IGeometryContext2 context2) + context2.LineTo(point, isStroked); + else + _impl.LineTo(point); + + _currentPoint = point; + } + + public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked) + { + if (_impl is IGeometryContext2 context2) + context2.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection, isStroked); + else + _impl.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection); + + _currentPoint = point; + } + + public void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked) + { + if (_impl is IGeometryContext2 context2) + context2.CubicBezierTo(controlPoint1, controlPoint2, endPoint, isStroked); + else + _impl.CubicBezierTo(controlPoint1, controlPoint2, endPoint); + + _currentPoint = endPoint; + } + + public void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked) + { + if (_impl is IGeometryContext2 context2) + context2.QuadraticBezierTo(controlPoint, endPoint, isStroked); + else + _impl.QuadraticBezierTo(controlPoint, endPoint); + + _currentPoint = endPoint; + } } } diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index 8661959aa6..beac28da4a 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -182,18 +182,18 @@ namespace Avalonia.Media break; } - var origin = new Point(); + var origin = baselineOrigin; switch (Location) { - case TextDecorationLocation.Baseline: - origin += glyphRun.BaselineOrigin; + case TextDecorationLocation.Overline: + origin += new Point(0, textMetrics.Ascent); break; case TextDecorationLocation.Strikethrough: - origin += new Point(baselineOrigin.X, baselineOrigin.Y + textMetrics.StrikethroughPosition); + origin += new Point(0, textMetrics.StrikethroughPosition); break; case TextDecorationLocation.Underline: - origin += new Point(baselineOrigin.X, baselineOrigin.Y + textMetrics.UnderlinePosition); + origin += new Point(0, textMetrics.UnderlinePosition); break; } @@ -255,7 +255,10 @@ namespace Avalonia.Media } } - drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Metrics.Width, 0)); + var p1 = origin; + var p2 = p1 + new Point(glyphRun.Metrics.Width, 0); + + drawingContext.DrawLine(pen, p1, p2); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index a1dc0827e8..caaaa00780 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -98,7 +98,7 @@ namespace Avalonia.Media.TextFormatting /// /// true if characters fit into the available width; otherwise, false. /// - internal bool TryMeasureCharacters(double availableWidth, out int length) + public bool TryMeasureCharacters(double availableWidth, out int length) { length = 0; var currentWidth = 0.0; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index 7cdf81ecc9..e1d9253415 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -27,5 +27,56 @@ namespace Avalonia.Media.TextFormatting /// /// Text line to collapse. public abstract TextRun[]? Collapse(TextLine textLine); + + /// + /// Creates a list of runs for given collapsed length which includes specified symbol at the end. + /// + /// The text line. + /// The collapsed length. + /// The flow direction. + /// The symbol. + /// List of remaining runs. + public static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength, + FlowDirection flowDirection, TextRun shapedSymbol) + { + var textRuns = textLine.TextRuns; + + if (collapsedLength <= 0) + { + return new[] { shapedSymbol }; + } + + if (flowDirection == FlowDirection.RightToLeft) + { + collapsedLength = textLine.Length - collapsedLength; + } + + var objectPool = FormattingObjectPool.Instance; + + var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); + + try + { + if (flowDirection == FlowDirection.RightToLeft) + { + var collapsedRuns = new TextRun[postSplitRuns!.Count + 1]; + postSplitRuns.CopyTo(collapsedRuns, 1); + collapsedRuns[0] = shapedSymbol; + return collapsedRuns; + } + else + { + var collapsedRuns = new TextRun[preSplitRuns!.Count + 1]; + preSplitRuns.CopyTo(collapsedRuns); + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; + return collapsedRuns; + } + } + finally + { + objectPool.TextRunLists.Return(ref preSplitRuns); + objectPool.TextRunLists.Return(ref postSplitRuns); + } + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 8b6d576c6e..fb01afa33d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Media.TextFormatting.Unicode; namespace Avalonia.Media.TextFormatting { @@ -17,12 +16,12 @@ namespace Avalonia.Media.TextFormatting var runIndex = 0; var currentWidth = 0.0; var collapsedLength = 0; - var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight); + var shapedSymbol = TextFormatter.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight); if (properties.Width < shapedSymbol.GlyphRun.Bounds.Width) { //Not enough space to fit in the symbol - return Array.Empty(); + return []; } var availableWidth = properties.Width - shapedSymbol.Size.Width; @@ -72,7 +71,7 @@ namespace Avalonia.Media.TextFormatting collapsedLength += measuredLength; - return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); + return TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); } availableWidth -= shapedRun.Size.Width; @@ -85,7 +84,7 @@ namespace Avalonia.Media.TextFormatting //The whole run needs to fit into available space if (currentWidth + drawableRun.Size.Width > availableWidth) { - return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); + return TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); } availableWidth -= drawableRun.Size.Width; @@ -146,7 +145,7 @@ namespace Avalonia.Media.TextFormatting collapsedLength += measuredLength; - return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); + return TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); } availableWidth -= shapedRun.Size.Width; @@ -159,7 +158,7 @@ namespace Avalonia.Media.TextFormatting //The whole run needs to fit into available space if (currentWidth + drawableRun.Size.Width > availableWidth) { - return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); + return TextCollapsingProperties.CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); } availableWidth -= drawableRun.Size.Width; @@ -176,48 +175,5 @@ namespace Avalonia.Media.TextFormatting return null; } - - private static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength, - FlowDirection flowDirection, TextRun shapedSymbol) - { - var textRuns = textLine.TextRuns; - - if (collapsedLength <= 0) - { - return new[] { shapedSymbol }; - } - - if(flowDirection == FlowDirection.RightToLeft) - { - collapsedLength = textLine.Length - collapsedLength; - } - - var objectPool = FormattingObjectPool.Instance; - - var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); - - try - { - if (flowDirection == FlowDirection.RightToLeft) - { - var collapsedRuns = new TextRun[postSplitRuns!.Count + 1]; - postSplitRuns.CopyTo(collapsedRuns, 1); - collapsedRuns[0] = shapedSymbol; - return collapsedRuns; - } - else - { - var collapsedRuns = new TextRun[preSplitRuns!.Count + 1]; - preSplitRuns.CopyTo(collapsedRuns); - collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; - return collapsedRuns; - } - } - finally - { - objectPool.TextRunLists.Return(ref preSplitRuns); - objectPool.TextRunLists.Return(ref postSplitRuns); - } - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs index ff8c1c4860..1dbf55fb97 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs @@ -40,5 +40,31 @@ /// The formatted line. public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null); + + /// + /// Creates a shaped symbol. + /// + /// The symbol run to shape. + /// The flow direction. + /// + /// The shaped symbol. + /// + public static ShapedTextRun CreateSymbol(TextRun textRun, FlowDirection flowDirection) + { + var textShaper = TextShaper.Current; + + var glyphTypeface = textRun.Properties!.CachedGlyphTypeface; + + var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; + + var cultureInfo = textRun.Properties.CultureInfo; + + var shaperOptions = new TextShaperOptions(glyphTypeface, textRun.Properties.FontFeatures, + fontRenderingEmSize, (sbyte)flowDirection, cultureInfo); + + var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions); + + return new ShapedTextRun(shapedBuffer, textRun.Properties); + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 5aeff0fba2..8e2325fb14 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -957,31 +957,5 @@ namespace Avalonia.Media.TextFormatting return true; } } - - /// - /// Creates a shaped symbol. - /// - /// The symbol run to shape. - /// The flow direction. - /// - /// The shaped symbol. - /// - internal static ShapedTextRun CreateSymbol(TextRun textRun, FlowDirection flowDirection) - { - var textShaper = TextShaper.Current; - - var glyphTypeface = textRun.Properties!.CachedGlyphTypeface; - - var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; - - var cultureInfo = textRun.Properties.CultureInfo; - - var shaperOptions = new TextShaperOptions(glyphTypeface, textRun.Properties.FontFeatures, - fontRenderingEmSize, (sbyte)flowDirection, cultureInfo); - - var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions); - - return new ShapedTextRun(shapedBuffer, textRun.Properties); - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index f60440e6b1..c6da172604 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -96,7 +96,7 @@ namespace Avalonia.Media.TextFormatting { case DrawableTextRun drawableTextRun: { - var offsetY = GetBaselineOffset(this, drawableTextRun); + var offsetY = GetBaselineOffset(drawableTextRun); drawableTextRun.Draw(drawingContext, new Point(currentX, currentY + offsetY)); @@ -108,28 +108,38 @@ namespace Avalonia.Media.TextFormatting } } - private static double GetBaselineOffset(TextLine textLine, DrawableTextRun textRun) + private double GetBaselineOffset(DrawableTextRun textRun) { var baseline = textRun.Baseline; var baselineAlignment = textRun.Properties?.BaselineAlignment; + var baselineOffset = -baseline; + switch (baselineAlignment) { + case BaselineAlignment.Baseline: + baselineOffset += Baseline; + break; case BaselineAlignment.Top: - return 0; + case BaselineAlignment.TextTop: + baselineOffset += Height - Extent + textRun.Size.Height / 2; + break; case BaselineAlignment.Center: - return textLine.Height / 2 - textRun.Size.Height / 2; + baselineOffset += Height / 2 + baseline - textRun.Size.Height / 2; + break; + case BaselineAlignment.Subscript: case BaselineAlignment.Bottom: - return textLine.Height - textRun.Size.Height; - case BaselineAlignment.Baseline: - case BaselineAlignment.TextTop: case BaselineAlignment.TextBottom: - case BaselineAlignment.Subscript: + baselineOffset += Height - textRun.Size.Height + baseline; + break; case BaselineAlignment.Superscript: - return textLine.Baseline - baseline; + baselineOffset += baseline; + break; default: throw new ArgumentOutOfRangeException(nameof(baselineAlignment), baselineAlignment, null); } + + return baselineOffset; } /// @@ -731,23 +741,26 @@ namespace Avalonia.Media.TextFormatting } } - if (coveredLength > 0) + if (coveredLength == 0) { - if (lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds)) - { - currentBounds = lastBounds; - - result[result.Count - 1] = currentBounds; - } - else - { - result.Add(currentBounds); - } + //This should never happen + break; + } - lastBounds = currentBounds; + if (lastBounds != null && TryMergeWithLastBounds(currentBounds, lastBounds)) + { + currentBounds = lastBounds; - remainingLength -= coveredLength; + result[result.Count - 1] = currentBounds; + } + else + { + result.Add(currentBounds); } + + lastBounds = currentBounds; + + remainingLength -= coveredLength; } result.Sort(TextBoundsComparer); @@ -1017,10 +1030,23 @@ namespace Avalonia.Media.TextFormatting var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); - //Make sure we properly deal with zero width space runs - if (characterLength == 0 && currentRun.Length > 0 && currentRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace == 0) + if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) { - characterLength = currentRun.Length; + //Make sure we are properly dealing with zero width space runs + var codepointEnumerator = new CodepointEnumerator(currentRun.Text.Span.Slice(startIndex)); + + while (remainingLength > 0 && codepointEnumerator.MoveNext(out var codepoint)) + { + if (codepoint.IsWhiteSpace) + { + characterLength++; + remainingLength--; + } + else + { + break; + } + } } if (endX < startX) @@ -1073,13 +1099,26 @@ namespace Avalonia.Media.TextFormatting var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); - //Make sure we properly deal with zero width space runs - if (characterLength == 0 && currentRun.Length > 0 && currentRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace == 0) + if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length) { - characterLength = currentRun.Length; + //Make sure we are properly dealing with zero width space runs + var codepointEnumerator = new CodepointEnumerator(currentRun.Text.Span.Slice(startIndex)); + + while (remainingLength > 0 && codepointEnumerator.MoveNext(out var codepoint)) + { + if (codepoint.IsWhiteSpace) + { + characterLength++; + remainingLength--; + } + else + { + break; + } + } } - if(startHit.FirstCharacterIndex > endHit.FirstCharacterIndex) + if (startHit.FirstCharacterIndex > endHit.FirstCharacterIndex) { startHit = endHit; } @@ -1143,7 +1182,6 @@ namespace Avalonia.Media.TextFormatting } TextRun? currentRun = null; - TextRun? previousRun = null; while (runIndex < _indexedTextRuns.Count) { @@ -1182,7 +1220,7 @@ namespace Avalonia.Media.TextFormatting break; } - case TextRun: + case not null: { if(direction == LogicalDirection.Forward) { @@ -1212,8 +1250,6 @@ namespace Avalonia.Media.TextFormatting } runIndex++; - - previousRun = currentRun; } return currentRun; @@ -1242,61 +1278,57 @@ namespace Avalonia.Media.TextFormatting switch (_textRuns[index]) { case ShapedTextRun textRun: - { - var textMetrics = textRun.TextMetrics; - var glyphRun = textRun.GlyphRun; - var runBounds = glyphRun.InkBounds.WithX(widthIncludingWhitespace + glyphRun.InkBounds.X); + { + var textMetrics = textRun.TextMetrics; + var glyphRun = textRun.GlyphRun; + var runBounds = glyphRun.InkBounds.WithX(widthIncludingWhitespace + glyphRun.InkBounds.X); - bounds = bounds.Union(runBounds); + bounds = bounds.Union(runBounds); - if (fontRenderingEmSize < textMetrics.FontRenderingEmSize) - { - fontRenderingEmSize = textMetrics.FontRenderingEmSize; + if (ascent > textMetrics.Ascent) + { + ascent = textMetrics.Ascent; + } - if (ascent > textMetrics.Ascent) - { - ascent = textMetrics.Ascent; - } + if (descent < textMetrics.Descent) + { + descent = textMetrics.Descent; + } - if (descent < textMetrics.Descent) - { - descent = textMetrics.Descent; - } + if (lineGap < textMetrics.LineGap) + { + lineGap = textMetrics.LineGap; + } - if (lineGap < textMetrics.LineGap) - { - lineGap = textMetrics.LineGap; - } + if (descent - ascent + lineGap > height) + { + height = descent - ascent + lineGap; + } - if (descent - ascent + lineGap > height) - { - height = descent - ascent + lineGap; - } - } - widthIncludingWhitespace += textRun.Size.Width; + widthIncludingWhitespace += textRun.Size.Width; - break; - } + break; + } case DrawableTextRun drawableTextRun: + { + widthIncludingWhitespace += drawableTextRun.Size.Width; + + if (drawableTextRun.Size.Height > height) { - widthIncludingWhitespace += drawableTextRun.Size.Width; + height = drawableTextRun.Size.Height; + } - if (drawableTextRun.Size.Height > height) - { - height = drawableTextRun.Size.Height; - } + //Adjust current ascent so drawables and text align at the bottom edge of the line. + var offset = Math.Max(0, drawableTextRun.Baseline + ascent - descent); - if (ascent > -drawableTextRun.Baseline) - { - ascent = -drawableTextRun.Baseline; - } + ascent -= offset; - bounds = bounds.Union(new Rect(new Point(bounds.Right, 0), drawableTextRun.Size)); + bounds = bounds.Union(new Rect(new Point(bounds.Right, 0), drawableTextRun.Size)); - break; - } + break; + } } } diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs index 8363b82400..d9bd158886 100644 --- a/src/Avalonia.Base/Media/Typeface.cs +++ b/src/Avalonia.Base/Media/Typeface.cs @@ -31,7 +31,7 @@ namespace Avalonia.Media throw new ArgumentException("Font stretch must be > 1."); } - FontFamily = fontFamily; + FontFamily = fontFamily ?? FontFamily.Default; Style = style; Weight = weight; Stretch = stretch; diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 15d4f39d6c..1e315688e9 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Collections.Pooled; using Avalonia.Media.Immutable; using Avalonia.Rendering; using Avalonia.Rendering.Composition; @@ -62,31 +63,65 @@ namespace Avalonia.Media internal override Func Factory => static c => new ServerCompositionSimpleContentBrush(c.Server); - private InlineDictionary _renderDataDictionary; - - private protected override void OnReferencedFromCompositor(Compositor c) + class RenderDataItem(CompositionRenderData data, Rect rect) : IDisposable { - _renderDataDictionary.Add(c, CreateServerContent(c)); - base.OnReferencedFromCompositor(c); + public CompositionRenderData Data { get; } = data; + public Rect Rect { get; } = rect; + public bool IsDirty; + public void Dispose() => Data?.Dispose(); } - + + private InlineDictionary _renderDataDictionary; + protected override void OnUnreferencedFromCompositor(Compositor c) { if (_renderDataDictionary.TryGetAndRemoveValue(c, out var content)) - content?.RenderData.Dispose(); + content?.Dispose(); base.OnUnreferencedFromCompositor(c); } private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer) { base.SerializeChanges(c, writer); - if (_renderDataDictionary.TryGetValue(c, out var content)) - writer.WriteObject(content); - else - writer.WriteObject(null); + CompositionRenderDataSceneBrushContent.Properties? content = null; + if (IsOnCompositor(c)) // Should always be true here, but just in case do this check + { + _renderDataDictionary.TryGetValue(c, out var data); + if (data == null || data.IsDirty) + { + var created = CreateServerContent(c); + // Dispose the old render list _after_ creating a new one to avoid unnecessary detach/attach + // sequence for referenced resources + if (data != null) + data.Dispose(); + + _renderDataDictionary[c] = data = created; + } + + if (data != null) + content = new(data.Data.Server, data.Rect, false); + } + + writer.WriteObject(content); } - CompositionRenderDataSceneBrushContent? CreateServerContent(Compositor c) + void InvalidateContent() + { + foreach(var item in _renderDataDictionary) + if (item.Value != null) + item.Value.IsDirty = true; + RegisterForSerialization(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + // We are supposed to be only calling this when content is actually changed, + // but instead we are calling this on brush property change for backwards compat with 0.10.x + InvalidateContent(); + base.OnPropertyChanged(change); + } + + RenderDataItem? CreateServerContent(Compositor c) { if (Visual == null) return null; @@ -99,10 +134,8 @@ namespace Avalonia.Media var renderData = recorder.GetRenderResults(); if (renderData == null) return null; - - return new CompositionRenderDataSceneBrushContent( - (ServerCompositionSimpleContentBrush)((ICompositionRenderResource)this).GetForCompositor(c), - renderData, new(Visual.Bounds.Size), false); + + return new(renderData, new(Visual.Bounds.Size)); } } } diff --git a/src/Avalonia.Base/Metadata/InheritDataTypeFromAttribute.cs b/src/Avalonia.Base/Metadata/InheritDataTypeFromAttribute.cs new file mode 100644 index 0000000000..4a57667f82 --- /dev/null +++ b/src/Avalonia.Base/Metadata/InheritDataTypeFromAttribute.cs @@ -0,0 +1,44 @@ +using System; + +namespace Avalonia.Metadata; + +/// +/// Represents the kind of scope from which a data type can be inherited. Used in resolving target for AvaloniaProperty. +/// +public enum InheritDataTypeFromScopeKind +{ + /// + /// Indicates that the data type should be inherited from a style. + /// + Style = 1, + + /// + /// Indicates that the data type should be inherited from a control template. + /// + ControlTemplate, +} + +/// +/// Attribute that instructs the compiler to resolve the data type using specific scope hints, such as Style or ControlTemplate. +/// +/// +/// This attribute is used to configure markup extensions like TemplateBinding to properly parse AvaloniaProperty values, +/// targeting a specific scope data type. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] +public sealed class InheritDataTypeFromAttribute : Attribute +{ + /// + /// Initializes a new instance of the class with the specified scope kind. + /// + /// The kind of scope from which to inherit the data type. + public InheritDataTypeFromAttribute(InheritDataTypeFromScopeKind scopeKind) + { + ScopeKind = scopeKind; + } + + /// + /// Gets the kind of scope from which the data type should be inherited. + /// + public InheritDataTypeFromScopeKind ScopeKind { get; } +} diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 9621037cc1..476bca5a33 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -229,4 +229,10 @@ namespace Avalonia.Platform /// bool CanBlit { get; } } + + public interface IDrawingContextLayerWithRenderContextAffinityImpl : IDrawingContextLayerImpl + { + bool HasRenderContextAffinity { get; } + IBitmapImpl CreateNonAffinedSnapshot(); + } } diff --git a/src/Avalonia.Base/Platform/IGeometryContext2.cs b/src/Avalonia.Base/Platform/IGeometryContext2.cs new file mode 100644 index 0000000000..4142430e9d --- /dev/null +++ b/src/Avalonia.Base/Platform/IGeometryContext2.cs @@ -0,0 +1,46 @@ +using Avalonia.Media; + +namespace Avalonia.Platform +{ + // TODO12 combine with IGeometryContext + public interface IGeometryContext2 : IGeometryContext + { + /// + /// Draws a line to the specified point. + /// + /// The destination point. + /// Whether the segment is stroked + void LineTo(Point point, bool isStroked); + + /// + /// Draws an arc to the specified point. + /// + /// The destination point. + /// The radii of an oval whose perimeter is used to draw the angle. + /// The rotation angle (in radians) of the oval that specifies the curve. + /// true to draw the arc greater than 180 degrees; otherwise, false. + /// + /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. + /// + /// Whether the segment is stroked + void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked); + + /// + /// Draws a Bezier curve to the specified point. + /// + /// The first control point used to specify the shape of the curve. + /// The second control point used to specify the shape of the curve. + /// The destination point for the end of the curve. + /// Whether the segment is stroked + void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked); + + /// + /// Draws a quadratic Bezier curve to the specified point + /// + /// Control point + /// DestinationPoint + /// Whether the segment is stroked + void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked); + } + +} diff --git a/src/Avalonia.Base/Platform/IPlatformGpu.cs b/src/Avalonia.Base/Platform/IPlatformGpu.cs index f6953619d2..b9f11c3ebb 100644 --- a/src/Avalonia.Base/Platform/IPlatformGpu.cs +++ b/src/Avalonia.Base/Platform/IPlatformGpu.cs @@ -11,6 +11,19 @@ public interface IPlatformGraphics IPlatformGraphicsContext GetSharedContext(); } +[Unstable] +public interface IPlatformGraphicsWithFeatures : IPlatformGraphics, IOptionalFeatureProvider +{ + +} + +[Unstable] +public interface IPlatformGraphicsReadyStateFeature +{ + bool IsReady { get; } + bool UsesContexts { get; } +} + [Unstable] public interface IPlatformGraphicsContext : IDisposable, IOptionalFeatureProvider { diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 0ead242a27..30b426489a 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -215,6 +215,14 @@ namespace Avalonia.Platform /// /// An . IRenderTarget CreateRenderTarget(IEnumerable surfaces); + + /// + /// Creates an offscreen render target + /// + /// The size, in pixels, of the render target + /// The scaling which will be reported by IBitmap.Dpi + /// + IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, double scaling); /// /// Indicates that the context is no longer usable. This method should be thread-safe diff --git a/src/Avalonia.Base/Platform/IScopedResource.cs b/src/Avalonia.Base/Platform/IScopedResource.cs new file mode 100644 index 0000000000..616b211182 --- /dev/null +++ b/src/Avalonia.Base/Platform/IScopedResource.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; + +namespace Avalonia.Platform; + +public interface IScopedResource : IDisposable +{ + public T Value { get; } +} + +public class ScopedResource : IScopedResource +{ + private int _disposed = 0; + private T _value; + private Action? _dispose; + private ScopedResource(T value, Action dispose) + { + _value = value; + _dispose = dispose; + } + + public static IScopedResource Create(T value, Action dispose) => new ScopedResource(value, dispose); + + public void Dispose() + { + if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) + { + var disp = _dispose!; + _value = default!; + _dispose = null; + disp(); + } + } + + public T Value + { + get + { + if (_disposed == 1) + throw new ObjectDisposedException(this.GetType().FullName); + return _value; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs b/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs index 92ec820b2c..9a7582ca5f 100644 --- a/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs +++ b/src/Avalonia.Base/Platform/Internal/UnmanagedBlob.cs @@ -33,6 +33,7 @@ internal class UnmanagedBlob : IDisposable GC.WaitForPendingFinalizers(); } #endif + public static bool SuppressFinalizerWarning { get; set; } public UnmanagedBlob(int size) { @@ -78,7 +79,7 @@ internal class UnmanagedBlob : IDisposable public void Dispose() { #if DEBUG - if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId) + if (!SuppressFinalizerWarning && Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId) { lock (_lock) { diff --git a/src/Avalonia.Base/Platform/PlatformHandle.cs b/src/Avalonia.Base/Platform/PlatformHandle.cs index 9cc2741a12..79dd4f54dc 100644 --- a/src/Avalonia.Base/Platform/PlatformHandle.cs +++ b/src/Avalonia.Base/Platform/PlatformHandle.cs @@ -5,7 +5,7 @@ namespace Avalonia.Platform /// /// Represents a platform-specific handle. /// - public class PlatformHandle : IPlatformHandle + public class PlatformHandle : IPlatformHandle, IEquatable { /// /// Initializes a new instance of the class. @@ -29,5 +29,44 @@ namespace Avalonia.Platform /// Gets an optional string that describes what represents. /// public string? HandleDescriptor { get; } + + /// + public override string ToString() + { + return $"PlatformHandle {{ {HandleDescriptor} = {Handle} }}"; + } + + /// + public bool Equals(PlatformHandle? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return Handle == other.Handle && HandleDescriptor == other.HandleDescriptor; + } + + /// + public override bool Equals(object? obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((PlatformHandle)obj); + } + + /// + public override int GetHashCode() + { + return (Handle, HandleDescriptor).GetHashCode(); + } + + public static bool operator ==(PlatformHandle? left, PlatformHandle? right) + { + return Equals(left, right); + } + + public static bool operator !=(PlatformHandle? left, PlatformHandle? right) + { + return !Equals(left, right); + } } } diff --git a/src/Avalonia.Base/Platform/RenderTargetProperties.cs b/src/Avalonia.Base/Platform/RenderTargetProperties.cs index 33b117ff1e..c4a0948180 100644 --- a/src/Avalonia.Base/Platform/RenderTargetProperties.cs +++ b/src/Avalonia.Base/Platform/RenderTargetProperties.cs @@ -1,8 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using Avalonia.Metadata; namespace Avalonia.Platform; [PrivateApi] +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Private API, not meant to be compared")] public struct RenderTargetProperties { /// @@ -21,10 +23,11 @@ public struct RenderTargetProperties } [PrivateApi] +[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Private API, not meant to be compared")] public struct RenderTargetDrawingContextProperties { /// /// Indicates that the drawing context targets a surface that preserved its contents since the previous frame /// public bool PreviousFrameIsRetained { get; init; } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs new file mode 100644 index 0000000000..082a85f2b6 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FallbackStorageProvider.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Avalonia.Platform.Storage; +#pragma warning disable CA1823 + +internal class FallbackStorageProvider : IStorageProvider +{ + private readonly Func>[] _factories; + private readonly List _providers = new(); + private int _nextProviderFactory = 0; + + public FallbackStorageProvider(Func>[] factories) + { + _factories = factories; + } + + async IAsyncEnumerable GetProviders() + { + foreach (var p in _providers) + yield return p; + for (;_nextProviderFactory < _factories.Length;) + { + var p = await _factories[_nextProviderFactory](); + _nextProviderFactory++; + if (p != null) + { + _providers.Add(p); + yield return p; + } + } + } + + async Task GetFor(Func filter) + { + await foreach (var p in GetProviders()) + if (filter(p)) + return p; + throw new IOException("Unable to select a suitable storage provider"); + } + + + // Those should _really_ have been asynchronous, + // but this class is expected to fall back to the managed implementation anyway + public bool CanOpen => true; + public bool CanSave => true; + public bool CanPickFolder => true; + + public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) + { + return await (await GetFor(p => p.CanOpen)).OpenFilePickerAsync(options); + } + + public async Task SaveFilePickerAsync(FilePickerSaveOptions options) + { + return await (await GetFor(p => p.CanSave)).SaveFilePickerAsync(options); + } + + + public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) + { + return await (await GetFor(p => p.CanPickFolder)).OpenFolderPickerAsync(options); + } + + async Task FirstNotNull(TArg arg, Func> cb) + where TResult : class + { + await foreach (var p in GetProviders()) + { + var res = await cb(p, arg); + if (res != null) + return res; + } + + return null; + } + + public Task OpenFileBookmarkAsync(string bookmark) => + FirstNotNull(bookmark, (p, a) => p.OpenFileBookmarkAsync(a)); + + public Task OpenFolderBookmarkAsync(string bookmark) => + FirstNotNull(bookmark, (p, a) => p.OpenFolderBookmarkAsync(a)); + + public Task TryGetFileFromPathAsync(Uri filePath) => + FirstNotNull(filePath, (p, a) => p.TryGetFileFromPathAsync(filePath)); + + public Task TryGetFolderFromPathAsync(Uri folderPath) + => FirstNotNull(folderPath, (p, a) => p.TryGetFolderFromPathAsync(a)); + + public Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) => + FirstNotNull(wellKnownFolder, (p, a) => p.TryGetWellKnownFolderAsync(a)); + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index 715fc182d6..c465dddb84 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -1,115 +1,10 @@ -using System; -using System.IO; -using System.Security; +using System.IO; using System.Threading.Tasks; namespace Avalonia.Platform.Storage.FileIO; -internal class BclStorageFile : IStorageBookmarkFile +internal sealed class BclStorageFile(FileInfo fileInfo) : BclStorageItem(fileInfo), IStorageBookmarkFile { - public BclStorageFile(FileInfo fileInfo) - { - FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); - } - - public FileInfo FileInfo { get; } - - public string Name => FileInfo.Name; - - public virtual bool CanBookmark => true; - - public Uri Path - { - get - { - try - { - if (FileInfo.Directory is not null) - { - return StorageProviderHelpers.FilePathToUri(FileInfo.FullName); - } - } - catch (SecurityException) - { - } - return new Uri(FileInfo.Name, UriKind.Relative); - } - } - - public Task GetBasicPropertiesAsync() - { - if (FileInfo.Exists) - { - return Task.FromResult(new StorageItemProperties( - (ulong)FileInfo.Length, - FileInfo.CreationTimeUtc, - FileInfo.LastAccessTimeUtc)); - } - return Task.FromResult(new StorageItemProperties()); - } - - public Task GetParentAsync() - { - if (FileInfo.Directory is { } directory) - { - return Task.FromResult(new BclStorageFolder(directory)); - } - return Task.FromResult(null); - } - - public Task OpenReadAsync() - { - return Task.FromResult(FileInfo.OpenRead()); - } - - public Task OpenWriteAsync() - { - var stream = new FileStream(FileInfo.FullName, FileMode.Create, FileAccess.Write, FileShare.Write); - return Task.FromResult(stream); - } - - public virtual Task SaveBookmarkAsync() - { - return Task.FromResult(FileInfo.FullName); - } - - public Task ReleaseBookmarkAsync() - { - // No-op - return Task.CompletedTask; - } - - protected virtual void Dispose(bool disposing) - { - } - - ~BclStorageFile() - { - Dispose(disposing: false); - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - public Task DeleteAsync() - { - FileInfo.Delete(); - return Task.CompletedTask; - } - - public Task MoveAsync(IStorageFolder destination) - { - if (destination is BclStorageFolder storageFolder) - { - var newPath = System.IO.Path.Combine(storageFolder.DirectoryInfo.FullName, FileInfo.Name); - FileInfo.MoveTo(newPath); - - return Task.FromResult(new BclStorageFile(new FileInfo(newPath))); - } - - return Task.FromResult(null); - } + public Task OpenReadAsync() => Task.FromResult(OpenReadCore(fileInfo)); + public Task OpenWriteAsync() => Task.FromResult(OpenWriteCore(fileInfo)); } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index a5727dc3cd..05572d6058 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -1,128 +1,22 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; -using System.Security; using System.Threading.Tasks; using Avalonia.Utilities; namespace Avalonia.Platform.Storage.FileIO; -internal class BclStorageFolder : IStorageBookmarkFolder +internal sealed class BclStorageFolder(DirectoryInfo directoryInfo) + : BclStorageItem(directoryInfo), IStorageBookmarkFolder { - public BclStorageFolder(DirectoryInfo directoryInfo) - { - DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); - if (!DirectoryInfo.Exists) - { - throw new ArgumentException("Directory must exist", nameof(directoryInfo)); - } - } + public IAsyncEnumerable GetItemsAsync() => GetItemsCore(directoryInfo) + .Select(WrapFileSystemInfo) + .Where(f => f is not null) + .AsAsyncEnumerable()!; - public string Name => DirectoryInfo.Name; + public Task CreateFileAsync(string name) => Task.FromResult( + (IStorageFile?)WrapFileSystemInfo(CreateFileCore(directoryInfo, name))); - public DirectoryInfo DirectoryInfo { get; } - - public bool CanBookmark => true; - - public Uri Path - { - get - { - try - { - return StorageProviderHelpers.FilePathToUri(DirectoryInfo.FullName); - } - catch (SecurityException) - { - return new Uri(DirectoryInfo.Name, UriKind.Relative); - } - } - } - - public Task GetBasicPropertiesAsync() - { - var props = new StorageItemProperties( - null, - DirectoryInfo.CreationTimeUtc, - DirectoryInfo.LastAccessTimeUtc); - return Task.FromResult(props); - } - - public Task GetParentAsync() - { - if (DirectoryInfo.Parent is { } directory) - { - return Task.FromResult(new BclStorageFolder(directory)); - } - return Task.FromResult(null); - } - - public IAsyncEnumerable GetItemsAsync() - => DirectoryInfo.EnumerateDirectories() - .Select(d => (IStorageItem)new BclStorageFolder(d)) - .Concat(DirectoryInfo.EnumerateFiles().Select(f => new BclStorageFile(f))) - .AsAsyncEnumerable(); - - public virtual Task SaveBookmarkAsync() - { - return Task.FromResult(DirectoryInfo.FullName); - } - - public Task ReleaseBookmarkAsync() - { - // No-op - return Task.CompletedTask; - } - - protected virtual void Dispose(bool disposing) - { - } - - ~BclStorageFolder() - { - Dispose(disposing: false); - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - public Task DeleteAsync() - { - DirectoryInfo.Delete(true); - return Task.CompletedTask; - } - - public Task MoveAsync(IStorageFolder destination) - { - if (destination is BclStorageFolder storageFolder) - { - var newPath = System.IO.Path.Combine(storageFolder.DirectoryInfo.FullName, DirectoryInfo.Name); - DirectoryInfo.MoveTo(newPath); - - return Task.FromResult(new BclStorageFolder(new DirectoryInfo(newPath))); - } - - return Task.FromResult(null); - } - - public Task CreateFileAsync(string name) - { - var fileName = System.IO.Path.Combine(DirectoryInfo.FullName, name); - var newFile = new FileInfo(fileName); - - using var stream = newFile.Create(); - - return Task.FromResult(new BclStorageFile(newFile)); - } - - public Task CreateFolderAsync(string name) - { - var newFolder = DirectoryInfo.CreateSubdirectory(name); - - return Task.FromResult(new BclStorageFolder(newFolder)); - } + public Task CreateFolderAsync(string name) => Task.FromResult( + (IStorageFolder?)WrapFileSystemInfo(CreateFolderCore(directoryInfo, name))); } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs new file mode 100644 index 0000000000..123d0e9283 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageItem.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Security; +using System.Threading.Tasks; + +namespace Avalonia.Platform.Storage.FileIO; + +internal abstract class BclStorageItem(FileSystemInfo fileSystemInfo) : IStorageBookmarkItem, IStorageItemWithFileSystemInfo +{ + public FileSystemInfo FileSystemInfo { get; } = fileSystemInfo switch + { + null => throw new ArgumentNullException(nameof(fileSystemInfo)), + DirectoryInfo { Exists: false } => throw new ArgumentException("Directory must exist", nameof(fileSystemInfo)), + _ => fileSystemInfo + }; + + public string Name => FileSystemInfo.Name; + + public bool CanBookmark => true; + + public Uri Path => GetPathCore(FileSystemInfo); + + public Task GetBasicPropertiesAsync() + { + return Task.FromResult(GetBasicPropertiesAsyncCore(FileSystemInfo)); + } + + public Task GetParentAsync() => Task.FromResult( + (IStorageFolder?)WrapFileSystemInfo(GetParentCore(FileSystemInfo))); + + public Task DeleteAsync() + { + DeleteCore(FileSystemInfo); + return Task.CompletedTask; + } + + public Task MoveAsync(IStorageFolder destination) => Task.FromResult( + WrapFileSystemInfo(MoveCore(FileSystemInfo, destination))); + + public Task SaveBookmarkAsync() + { + var path = FileSystemInfo.FullName; + return Task.FromResult(StorageBookmarkHelper.EncodeBclBookmark(path)); + } + + public Task ReleaseBookmarkAsync() => Task.CompletedTask; + + public void Dispose() { } + + [return: NotNullIfNotNull(nameof(fileSystemInfo))] + protected IStorageItem? WrapFileSystemInfo(FileSystemInfo? fileSystemInfo) => fileSystemInfo switch + { + DirectoryInfo directoryInfo => new BclStorageFolder(directoryInfo), + FileInfo fileInfo => new BclStorageFile(fileInfo), + _ => null + }; + + internal static void DeleteCore(FileSystemInfo fileSystemInfo) => fileSystemInfo.Delete(); + + internal static Uri GetPathCore(FileSystemInfo fileSystemInfo) + { + try + { + if (fileSystemInfo is DirectoryInfo { Parent: not null } or FileInfo { Directory: not null }) + { + return StorageProviderHelpers.UriFromFilePath(fileSystemInfo.FullName, fileSystemInfo is DirectoryInfo); + } + } + catch (SecurityException) + { + } + + return new Uri(fileSystemInfo.Name, UriKind.Relative); + } + + internal static StorageItemProperties GetBasicPropertiesAsyncCore(FileSystemInfo fileSystemInfo) + { + if (fileSystemInfo.Exists) + { + return new StorageItemProperties( + fileSystemInfo is FileInfo fileInfo ? (ulong)fileInfo.Length : 0, + fileSystemInfo.CreationTimeUtc, + fileSystemInfo.LastAccessTimeUtc); + } + + return new StorageItemProperties(); + } + + internal static DirectoryInfo? GetParentCore(FileSystemInfo fileSystemInfo) => fileSystemInfo switch + { + FileInfo { Directory: { } directory } => directory, + DirectoryInfo { Parent: { } parent } => parent, + _ => null + }; + + internal static FileSystemInfo? MoveCore(FileSystemInfo fileSystemInfo, IStorageFolder destination) + { + if (destination?.TryGetLocalPath() is { } destinationPath) + { + var newPath = System.IO.Path.Combine(destinationPath, fileSystemInfo.Name); + if (fileSystemInfo is DirectoryInfo directoryInfo) + { + directoryInfo.MoveTo(newPath); + return new DirectoryInfo(newPath); + } + + if (fileSystemInfo is FileInfo fileInfo) + { + fileInfo.MoveTo(newPath); + return new FileInfo(newPath); + } + } + + return null; + } + + internal static FileStream OpenReadCore(FileInfo fileInfo) => fileInfo.OpenRead(); + + internal static FileStream OpenWriteCore(FileInfo fileInfo) => + new(fileInfo.FullName, FileMode.Create, FileAccess.Write, FileShare.Write); + + internal static IEnumerable GetItemsCore(DirectoryInfo directoryInfo) => directoryInfo + .EnumerateDirectories() + .OfType() + .Concat(directoryInfo.EnumerateFiles()); + + internal static FileInfo CreateFileCore(DirectoryInfo directoryInfo, string name) + { + var fileName = System.IO.Path.Combine(directoryInfo.FullName, name); + var newFile = new FileInfo(fileName); + + using var stream = newFile.Create(); + return newFile; + } + + internal static DirectoryInfo CreateFolderCore(DirectoryInfo directoryInfo, string name) => + directoryInfo.CreateSubdirectory(name); +} diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs index 34409f5fda..fd2f499d06 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs @@ -4,6 +4,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Compatibility; +using Avalonia.Logging; namespace Avalonia.Platform.Storage.FileIO; @@ -20,18 +21,12 @@ internal abstract class BclStorageProvider : IStorageProvider public virtual Task OpenFileBookmarkAsync(string bookmark) { - var file = new FileInfo(bookmark); - return file.Exists - ? Task.FromResult(new BclStorageFile(file)) - : Task.FromResult(null); + return Task.FromResult(OpenBookmark(bookmark) as IStorageBookmarkFile); } public virtual Task OpenFolderBookmarkAsync(string bookmark) { - var folder = new DirectoryInfo(bookmark); - return folder.Exists - ? Task.FromResult(new BclStorageFolder(folder)) - : Task.FromResult(null); + return Task.FromResult(OpenBookmark(bookmark) as IStorageBookmarkFolder); } public virtual Task TryGetFileFromPathAsync(Uri filePath) @@ -63,6 +58,16 @@ internal abstract class BclStorageProvider : IStorageProvider } public virtual Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) + { + if (TryGetWellKnownFolderCore(wellKnownFolder) is { } directoryInfo) + { + return Task.FromResult(new BclStorageFolder(directoryInfo)); + } + + return Task.FromResult(null); + } + + internal static DirectoryInfo? TryGetWellKnownFolderCore(WellKnownFolder wellKnownFolder) { // Note, this BCL API returns different values depending on the .NET version. // We should also document it. @@ -82,16 +87,16 @@ internal abstract class BclStorageProvider : IStorageProvider if (folderPath is null) { - return Task.FromResult(null); + return null; } var directory = new DirectoryInfo(folderPath); if (!directory.Exists) { - return Task.FromResult(null); + return null; } - - return Task.FromResult(new BclStorageFolder(directory)); + + return directory; string GetFromSpecialFolder(Environment.SpecialFolder folder) => Environment.GetFolderPath(folder, Environment.SpecialFolderOption.Create); @@ -104,7 +109,7 @@ internal abstract class BclStorageProvider : IStorageProvider if (OperatingSystemEx.IsWindows()) { return Environment.OSVersion.Version.Major < 6 ? null : - SHGetKnownFolderPath(s_folderDownloads, 0, IntPtr.Zero); + Marshal.PtrToStringUni(SHGetKnownFolderPath(s_folderDownloads, 0, IntPtr.Zero)); } if (OperatingSystemEx.IsLinux()) @@ -123,8 +128,27 @@ internal abstract class BclStorageProvider : IStorageProvider return null; } - + + private IStorageBookmarkItem? OpenBookmark(string bookmark) + { + try + { + if (StorageBookmarkHelper.TryDecodeBclBookmark(bookmark, out var localPath)) + { + return StorageProviderHelpers.TryCreateBclStorageItem(localPath); + } + + return null; + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Information, LogArea.Platform)? + .Log(this, "Unable to read file bookmark: {Exception}", ex); + return null; + } + } + private static readonly Guid s_folderDownloads = new Guid("374DE290-123F-4565-9164-39C4925E467B"); - [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] - private static extern string SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid id, int flags, IntPtr token); + [DllImport("shell32.dll")] + private static extern IntPtr SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid id, int flags, IntPtr token); } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs b/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs new file mode 100644 index 0000000000..0e0ffa3b1b --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FileIO/SecurityScopedStream.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Platform.Storage.FileIO; + +/// +/// Stream wrapper currently used by Apple platforms, +/// where in sandboxed scenario it's advised to call [NSUri startAccessingSecurityScopedResource]. +/// +internal sealed class SecurityScopedStream(FileStream _stream, IDisposable _securityScope) : Stream +{ + public override bool CanRead => _stream.CanRead; + + public override bool CanSeek => _stream.CanSeek; + + public override bool CanWrite => _stream.CanWrite; + + public override long Length => _stream.Length; + + public override long Position + { + get => _stream.Position; + set => _stream.Position = value; + } + + public override void Flush() => + _stream.Flush(); + + public override Task FlushAsync(CancellationToken cancellationToken) => + _stream.FlushAsync(cancellationToken); + + public override int ReadByte() => + _stream.ReadByte(); + + public override int Read(byte[] buffer, int offset, int count) => + _stream.Read(buffer, offset, count); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + _stream.ReadAsync(buffer, offset, count, cancellationToken); + +#if NET6_0_OR_GREATER + public override int Read(Span buffer) => _stream.Read(buffer); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => + _stream.ReadAsync(buffer, cancellationToken); +#endif + + public override void Write(byte[] buffer, int offset, int count) => + _stream.Write(buffer, offset, count); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + _stream.WriteAsync(buffer, offset, count, cancellationToken); + +#if NET6_0_OR_GREATER + public override void Write(ReadOnlySpan buffer) => _stream.Write(buffer); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => + _stream.WriteAsync(buffer, cancellationToken); +#endif + + public override void WriteByte(byte value) => _stream.WriteByte(value); + + public override long Seek(long offset, SeekOrigin origin) => + _stream.Seek(offset, origin); + + public override void SetLength(long value) => + _stream.SetLength(value); + +#if NET6_0_OR_GREATER + public override void CopyTo(Stream destination, int bufferSize) => _stream.CopyTo(destination, bufferSize); +#endif + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => + _stream.CopyToAsync(destination, bufferSize, cancellationToken); + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => + _stream.BeginRead(buffer, offset, count, callback, state); + + public override int EndRead(IAsyncResult asyncResult) => _stream.EndRead(asyncResult); + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => + _stream.BeginWrite(buffer, offset, count, callback, state); + + public override void EndWrite(IAsyncResult asyncResult) => _stream.EndWrite(asyncResult); + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + _stream.Dispose(); + } + } + finally + { + _securityScope.Dispose(); + } + } + +#if NET6_0_OR_GREATER + public override async ValueTask DisposeAsync() + { + try + { + await _stream.DisposeAsync(); + } + finally + { + _securityScope.Dispose(); + } + } +#endif +} diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs new file mode 100644 index 0000000000..a71dd6f3e6 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageBookmarkHelper.cs @@ -0,0 +1,153 @@ +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; + +namespace Avalonia.Platform.Storage.FileIO; + +/// +/// In order to have unique bookmarks across platforms, we prepend a platform specific suffix before native bookmark. +/// And always encoding them in base64 before returning to the user. +/// +/// +/// Bookmarks are encoded as: +/// 0-6 - avalonia prefix with version number +/// 7-15 - platform key +/// 16+ - native bookmark value +/// Which is then encoded in Base64. +/// +internal static class StorageBookmarkHelper +{ + private const int HeaderLength = 16; + private static ReadOnlySpan AvaHeaderPrefix => "ava.v1."u8; + private static ReadOnlySpan FakeBclBookmarkPlatform => "bcl"u8; + + [return: NotNullIfNotNull(nameof(nativeBookmark))] + public static string? EncodeBookmark(ReadOnlySpan platform, string? nativeBookmark) => + nativeBookmark is null ? null : EncodeBookmark(platform, Encoding.UTF8.GetBytes(nativeBookmark)); + + public static string? EncodeBookmark(ReadOnlySpan platform, ReadOnlySpan nativeBookmarkBytes) + { + if (nativeBookmarkBytes.Length == 0) + { + return null; + } + + if (platform.Length > HeaderLength) + { + throw new ArgumentException($"Platform name should not be longer than {HeaderLength} bytes", nameof(platform)); + } + + var arrayLength = HeaderLength + nativeBookmarkBytes.Length; + var arrayPool = ArrayPool.Shared.Rent(arrayLength); + try + { + // Write platform into first 16 bytes. + var arraySpan = arrayPool.AsSpan(0, arrayLength); + AvaHeaderPrefix.CopyTo(arraySpan); + platform.CopyTo(arraySpan.Slice(AvaHeaderPrefix.Length)); + + // Write bookmark bytes. + nativeBookmarkBytes.CopyTo(arraySpan.Slice(HeaderLength)); + + // We must use span overload because ArrayPool might return way too big array. +#if NET6_0_OR_GREATER + return Convert.ToBase64String(arraySpan); +#else + return Convert.ToBase64String(arraySpan.ToArray(), Base64FormattingOptions.None); +#endif + } + finally + { + ArrayPool.Shared.Return(arrayPool); + } + } + + public enum DecodeResult + { + Success = 0, + InvalidFormat, + InvalidPlatform + } + + public static DecodeResult TryDecodeBookmark(ReadOnlySpan platform, string? base64bookmark, out byte[]? nativeBookmark) + { + if (platform.Length > HeaderLength + || platform.Length == 0 + || base64bookmark is null + || base64bookmark.Length % 4 != 0) + { + nativeBookmark = null; + return DecodeResult.InvalidFormat; + } + + Span decodedBookmark; +#if NET6_0_OR_GREATER + // Each base64 character represents 6 bits, but to be safe, + var arrayPool = ArrayPool.Shared.Rent(HeaderLength + base64bookmark.Length * 6); + if (Convert.TryFromBase64Chars(base64bookmark, arrayPool, out int bytesWritten)) + { + decodedBookmark = arrayPool.AsSpan().Slice(0, bytesWritten); + } + else + { + nativeBookmark = null; + return DecodeResult.InvalidFormat; + } +#else + decodedBookmark = Convert.FromBase64String(base64bookmark).AsSpan(); +#endif + try + { + if (decodedBookmark.Length < HeaderLength + // Check if decoded string starts with the correct prefix, checking v1 at the same time. + && !AvaHeaderPrefix.SequenceEqual(decodedBookmark.Slice(0, AvaHeaderPrefix.Length))) + { + nativeBookmark = null; + return DecodeResult.InvalidFormat; + } + + var actualPlatform = decodedBookmark.Slice(AvaHeaderPrefix.Length, platform.Length); + if (!actualPlatform.SequenceEqual(platform)) + { + nativeBookmark = null; + return DecodeResult.InvalidPlatform; + } + + nativeBookmark = decodedBookmark.Slice(HeaderLength).ToArray(); + return DecodeResult.Success; + } + finally + { +#if NET6_0_OR_GREATER + ArrayPool.Shared.Return(arrayPool); +#endif + } + } + + public static string EncodeBclBookmark(string localPath) => EncodeBookmark(FakeBclBookmarkPlatform, localPath); + + public static bool TryDecodeBclBookmark(string nativeBookmark, [NotNullWhen(true)] out string? localPath) + { + var decodeResult = TryDecodeBookmark(FakeBclBookmarkPlatform, nativeBookmark, out var bytes); + if (decodeResult == DecodeResult.Success) + { + localPath = Encoding.UTF8.GetString(bytes!); + return true; + } + if (decodeResult == DecodeResult.InvalidFormat + && nativeBookmark.IndexOfAny(Path.GetInvalidPathChars()) < 0 + && !string.IsNullOrEmpty(Path.GetDirectoryName(nativeBookmark))) + { + // Attempt to restore old BCL bookmarks. + // Don't check for File.Exists here, as it will be done at later point in TryGetStorageItem. + // Just validate if it looks like a valid file path. + localPath = nativeBookmark; + return true; + } + + localPath = null; + return false; + } +} diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs index 76323eb900..48cc79672b 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs @@ -8,7 +8,7 @@ namespace Avalonia.Platform.Storage.FileIO; internal static class StorageProviderHelpers { - public static IStorageItem? TryCreateBclStorageItem(string path) + public static BclStorageItem? TryCreateBclStorageItem(string path) { if (!string.IsNullOrWhiteSpace(path)) { @@ -28,28 +28,36 @@ internal static class StorageProviderHelpers return null; } - public static Uri FilePathToUri(string path) + public static string? TryGetPathFromFileUri(Uri? uri) + { + // android "content:", browser and ios relative links are ignored. + return uri is { IsAbsoluteUri: true, Scheme: "file" } ? uri.LocalPath : null; + } + + public static Uri UriFromFilePath(string path, bool isDirectory) { var uriPath = new StringBuilder(path) .Replace("%", $"%{(int)'%':X2}") .Replace("[", $"%{(int)'[':X2}") - .Replace("]", $"%{(int)']':X2}") - .ToString(); + .Replace("]", $"%{(int)']':X2}"); + + if (!path.EndsWith('/') && isDirectory) + { + uriPath.Append('/'); + } - return new UriBuilder("file", string.Empty) { Path = uriPath }.Uri; + return new UriBuilder("file", string.Empty) { Path = uriPath.ToString() }.Uri; } - - public static bool TryFilePathToUri(string path, [NotNullWhen(true)] out Uri? uri) + + public static Uri? TryGetUriFromFilePath(string path, bool isDirectory) { try { - uri = FilePathToUri(path); - return true; + return UriFromFilePath(path, isDirectory); } catch { - uri = null; - return false; + return null; } } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs index 40f2720ee8..70c4f4bb0b 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs @@ -1,8 +1,14 @@ -using System.Threading.Tasks; +using System.IO; +using System.Threading.Tasks; using Avalonia.Metadata; namespace Avalonia.Platform.Storage; +internal interface IStorageItemWithFileSystemInfo : IStorageItem +{ + FileSystemInfo FileSystemInfo { get; } +} + [NotClientImplementable] public interface IStorageBookmarkItem : IStorageItem { diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs index 3933d23e4f..8f0c890ed1 100644 --- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs +++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs @@ -17,7 +17,7 @@ public static class StorageProviderExtensions return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile); } - if (StorageProviderHelpers.TryFilePathToUri(filePath, out var uri)) + if (StorageProviderHelpers.TryGetUriFromFilePath(filePath, false) is { } uri) { return provider.TryGetFileFromPathAsync(uri); } @@ -34,7 +34,7 @@ public static class StorageProviderExtensions return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder); } - if (StorageProviderHelpers.TryFilePathToUri(folderPath, out var uri)) + if (StorageProviderHelpers.TryGetUriFromFilePath(folderPath, true) is { } uri) { return provider.TryGetFolderFromPathAsync(uri); } @@ -56,21 +56,11 @@ public static class StorageProviderExtensions { // We can avoid double escaping of the path by checking for BclStorageFolder. // Ideally, `folder.Path.LocalPath` should also work, as that's only available way for the users. - if (item is BclStorageFolder storageFolder) + if (item is IStorageItemWithFileSystemInfo storageItem) { - return storageFolder.DirectoryInfo.FullName; - } - if (item is BclStorageFile storageFile) - { - return storageFile.FileInfo.FullName; - } - - if (item.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) - { - return absolutePath.LocalPath; + return storageItem.FileSystemInfo.FullName; } - // android "content:", browser and ios relative links go here. - return null; + return StorageProviderHelpers.TryGetPathFromFileUri(item.Path); } } diff --git a/src/Avalonia.Base/Points.cs b/src/Avalonia.Base/Points.cs index 2f88ecd80f..2e79f8e856 100644 --- a/src/Avalonia.Base/Points.cs +++ b/src/Avalonia.Base/Points.cs @@ -1,18 +1,20 @@ using System.Collections.Generic; using Avalonia.Collections; -namespace Avalonia +namespace Avalonia; + +/// +/// Represents a collection of values that can be individually accessed by index. +/// +public sealed class Points : AvaloniaList { - public sealed class Points : AvaloniaList + public Points() { - public Points() - { - - } + + } - public Points(IEnumerable points) : base(points) - { - - } + public Points(IEnumerable points) : base(points) + { + } } diff --git a/src/Avalonia.Base/PropertyStore/FramePriority.cs b/src/Avalonia.Base/PropertyStore/FramePriority.cs index 950a8375f2..a77bbe211b 100644 --- a/src/Avalonia.Base/PropertyStore/FramePriority.cs +++ b/src/Avalonia.Base/PropertyStore/FramePriority.cs @@ -28,6 +28,12 @@ namespace Avalonia.PropertyStore return (FramePriority)(p * 3 + (int)type); } + public static BindingPriority ToBindingPriority(this FramePriority priority) + { + var p = (int)priority / 3; + return p == 0 ? BindingPriority.Animation : (BindingPriority)p; + } + public static bool IsType(this FramePriority priority, FrameType type) { return (FrameType)((int)priority % 3) == type; diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 35406993c7..789383b860 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -292,6 +292,42 @@ namespace Avalonia.PropertyStore return property.GetDefaultValue(Owner); } + public BindingExpressionBase? GetExpression(AvaloniaProperty property) + { + var evaluatedLocalValue = false; + + bool TryGetLocalValue(out BindingExpressionBase? result) + { + if (!evaluatedLocalValue) + { + evaluatedLocalValue = true; + + if (_localValueBindings?.TryGetValue(property.Id, out var o) == true) + { + result = o as BindingExpressionBase; + return true; + } + } + + result = null; + return false; + } + + for (var i = _frames.Count - 1; i >= 0; --i) + { + var frame = _frames[i]; + + if (frame.Priority > BindingPriority.LocalValue && TryGetLocalValue(out var localExpression)) + return localExpression; + + if (frame.TryGetEntryIfActive(property, out var entry, out _)) + return entry as BindingExpressionBase; + } + + TryGetLocalValue(out var e); + return e; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static EffectiveValue CastEffectiveValue(EffectiveValue value) { @@ -468,7 +504,6 @@ namespace Avalonia.PropertyStore /// /// The binding entry. /// The priority of binding which produced a new value. - [Obsolete("TODO: Remove?")] public void OnBindingValueChanged( IValueEntry entry, BindingPriority priority) @@ -592,7 +627,6 @@ namespace Avalonia.PropertyStore /// /// The previously bound property. /// The observer. - [Obsolete("TODO: Remove?")] public void OnLocalValueBindingCompleted(AvaloniaProperty property, IDisposable observer) { if (_localValueBindings is not null && @@ -836,6 +870,40 @@ namespace Avalonia.PropertyStore } } + public ValueStoreDiagnostic GetStoreDiagnostic() + { + var frames = new List(); + + var effectiveLocalValues = new List(_effectiveValues.Count); + for (var i = 0; i < _effectiveValues.Count; i++) + { + if (_effectiveValues.GetValue(i) is { } effectiveValue + && effectiveValue.Priority == BindingPriority.LocalValue) + { + effectiveLocalValues.Add(new ValueEntryDiagnostic(effectiveValue.Property, effectiveValue.Value)); + } + } + + if (effectiveLocalValues.Count > 0) + { + frames.Add(new LocalValueFrameDiagnostic(effectiveLocalValues)); + } + + foreach (var frame in Frames) + { + if (frame is StyleInstance { Source: StyleBase } styleInstance) + { + frames.Add(new StyleValueFrameDiagnostic(styleInstance)); + } + else + { + frames.Add(new ValueFrameDiagnostic(frame)); + } + } + + return new ValueStoreDiagnostic(frames); + } + private int InsertFrame(ValueFrame frame) { Debug.Assert(!_frames.Contains(frame)); diff --git a/src/Avalonia.Base/Reactive/Operators/Switch.cs b/src/Avalonia.Base/Reactive/Operators/Switch.cs index bc849c499c..f0ca9e3ba2 100644 --- a/src/Avalonia.Base/Reactive/Operators/Switch.cs +++ b/src/Avalonia.Base/Reactive/Operators/Switch.cs @@ -54,6 +54,7 @@ internal sealed class Switch : IObservable var innerObserver = new InnerObserver(this, id); + _innerSerialDisposable?.Dispose(); _innerSerialDisposable = innerObserver; innerObserver.Disposable = value.Subscribe(innerObserver); } diff --git a/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleContentBrush.cs b/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleContentBrush.cs index 31eaf47925..673804f7bd 100644 --- a/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleContentBrush.cs +++ b/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleContentBrush.cs @@ -7,18 +7,21 @@ namespace Avalonia.Rendering.Composition.Server; internal sealed class ServerCompositionSimpleContentBrush : ServerCompositionSimpleTileBrush, ITileBrush, ISceneBrush { - private CompositionRenderDataSceneBrushContent? _content; + private CompositionRenderDataSceneBrushContent.Properties? _content; + internal ServerCompositionSimpleContentBrush(ServerCompositor compositor) : base(compositor) { } - // TODO: Figure out something about disposable - public ISceneBrushContent? CreateContent() => _content; + public ISceneBrushContent? CreateContent() => + _content == null || _content.RenderData.IsDisposed + ? null + : new CompositionRenderDataSceneBrushContent(this, _content); protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) { base.DeserializeChangesCore(reader, committedAt); - _content = reader.ReadObject(); + _content = reader.ReadObject(); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 48553d3b91..6b7a2ab081 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -5,6 +5,7 @@ using Avalonia.Animation; using Avalonia.Animation.Easings; using Avalonia.Controls; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Rendering.Composition.Server; @@ -34,6 +35,7 @@ namespace Avalonia.Rendering.Composition private CompositionBatch? _pendingBatch; private readonly object _pendingBatchLock = new(); private readonly List _pendingServerCompositorJobs = new(); + private readonly List _pendingServerCompositorPostTargetJobs = new(); private DiagnosticTextRenderer? _diagnosticTextRenderer; private readonly Action _triggerCommitRequested; @@ -170,14 +172,23 @@ namespace Avalonia.Rendering.Composition _disposeOnNextBatch.Clear(); } - if (_pendingServerCompositorJobs.Count > 0) + + static void SerializeServerJobs(BatchStreamWriter writer, List list, object startMarker, object endMarker) { - writer.WriteObject(ServerCompositor.RenderThreadJobsStartMarker); - foreach (var job in _pendingServerCompositorJobs) - writer.WriteObject(job); - writer.WriteObject(ServerCompositor.RenderThreadJobsEndMarker); + if (list.Count > 0) + { + writer.WriteObject(startMarker); + foreach (var job in list) + writer.WriteObject(job); + writer.WriteObject(endMarker); + } + list.Clear(); } - _pendingServerCompositorJobs.Clear(); + + SerializeServerJobs(writer, _pendingServerCompositorJobs, ServerCompositor.RenderThreadJobsStartMarker, + ServerCompositor.RenderThreadJobsEndMarker); + SerializeServerJobs(writer, _pendingServerCompositorPostTargetJobs, ServerCompositor.RenderThreadPostTargetJobsStartMarker, + ServerCompositor.RenderThreadPostTargetJobsEndMarker); } _nextCommit.CommittedAt = Server.Clock.Elapsed; @@ -227,21 +238,21 @@ namespace Avalonia.Rendering.Composition RequestCommitAsync(); } - internal void PostServerJob(Action job) + internal void PostServerJob(Action job, bool postTarget = false) { Dispatcher.VerifyAccess(); - _pendingServerCompositorJobs.Add(job); + (postTarget ? _pendingServerCompositorPostTargetJobs : _pendingServerCompositorJobs).Add(job); RequestCommitAsync(); } - internal Task InvokeServerJobAsync(Action job) => + internal Task InvokeServerJobAsync(Action job, bool postTarget = false) => InvokeServerJobAsync(() => { job(); return null; - }); + }, postTarget); - internal Task InvokeServerJobAsync(Func job) + internal Task InvokeServerJobAsync(Func job, bool postTarget = false) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); PostServerJob(() => @@ -254,7 +265,7 @@ namespace Avalonia.Rendering.Composition { tcs.TrySetException(e); } - }); + }, postTarget); return tcs.Task; } @@ -275,6 +286,16 @@ namespace Avalonia.Rendering.Composition (await GetRenderInterfacePublicFeatures().ConfigureAwait(false)).TryGetValue(featureType, out var rv); return rv; } + + public async Task CreateCompositionVisualSnapshot(CompositionVisual visual, double scaling) + { + if (visual.Compositor != this) + throw new InvalidOperationException(); + if (visual.Root == null) + throw new InvalidOperationException(); + var impl = await InvokeServerJobAsync(() => _server.CreateCompositionVisualSnapshot(visual.Server, scaling), true); + return new Bitmap(RefCountable.Create(impl)); + } /// /// Attempts to query for GPU interop feature from the platform render interface diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs index 4a46b07294..a8b9da743a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs @@ -6,20 +6,21 @@ namespace Avalonia.Rendering.Composition.Drawing; internal class CompositionRenderDataSceneBrushContent : ISceneBrushContent { - public CompositionRenderData RenderData { get; } + public ServerCompositionRenderData RenderData { get; } private readonly Rect? _rect; - public CompositionRenderDataSceneBrushContent(ITileBrush brush, CompositionRenderData renderData, Rect? rect, - bool useScalableRasterization) + public record Properties(ServerCompositionRenderData RenderData, Rect? Rect, bool UseScalableRasterization); + + public CompositionRenderDataSceneBrushContent(ITileBrush brush, Properties properties) { Brush = brush; - _rect = rect; - UseScalableRasterization = useScalableRasterization; - RenderData = renderData; + _rect = properties.Rect; + UseScalableRasterization = properties.UseScalableRasterization; + RenderData = properties.RenderData; } public ITileBrush Brush { get; } - public Rect Rect => _rect ?? (RenderData.Server?.Bounds?.ToRect() ?? default); + public Rect Rect => _rect ?? (RenderData?.Bounds?.ToRect() ?? default); public double Opacity => Brush.Opacity; public ITransform? Transform => Brush.Transform; @@ -36,11 +37,11 @@ internal class CompositionRenderDataSceneBrushContent : ISceneBrushContent { var oldTransform = context.Transform; context.Transform = transform.Value * oldTransform; - RenderData.Server.Render(context); + RenderData.Render(context); context.Transform = oldTransform; } else - RenderData.Server.Render(context); + RenderData.Render(context); } public bool UseScalableRasterization { get; } diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs index 11a0b9e227..d9f39d0ce0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using Avalonia.Rendering.Composition.Server; @@ -27,6 +28,7 @@ namespace Avalonia.Rendering.Composition.Expressions protected abstract string Print(); public override string ToString() => Print(); + [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.DesignTimeSupressWarningMessage)] internal static string OperatorName(ExpressionType t) { var attr = typeof(ExpressionType).GetMember(t.ToString())[0] diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 75dc13b1e0..f4124c22f0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -10,9 +10,6 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server; -/// - -/// internal partial class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport, IDrawingContextImplWithEffects { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs index 6071d7a5f2..69d317fff1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs @@ -16,14 +16,13 @@ namespace Avalonia.Rendering.Composition.Server private LtrbRect? _transformedContentBounds; private IImmutableEffect? _oldEffect; - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { - base.RenderCore(canvas, currentTransformedClip, dirtyRects); + base.RenderCore(context, currentTransformedClip); foreach (var ch in Children) { - ch.Render(canvas, currentTransformedClip, dirtyRects); + ch.Render(context, currentTransformedClip); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 3adf028438..ba9b042ad3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -40,17 +40,15 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua base.DeserializeChangesCore(reader, committedAt); } - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { if (_renderCommands != null - && currentTransformedClip.Intersects(TransformedOwnContentBounds) - && dirtyRects.Intersects(TransformedOwnContentBounds)) + && context.ShouldRenderOwnContent(this, currentTransformedClip)) { - _renderCommands.Render(canvas); + _renderCommands.Render(context.Canvas); } - base.RenderCore(canvas, currentTransformedClip, dirtyRects); + base.RenderCore(context, currentTransformedClip); } public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs index 20acf87d84..20a2501ca7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs @@ -5,18 +5,17 @@ namespace Avalonia.Rendering.Composition.Server; internal partial class ServerCompositionExperimentalAcrylicVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { var cornerRadius = CornerRadius; - canvas.DrawRectangle( + context.Canvas.DrawRectangle( Material, new RoundedRect( new Rect(0, 0, Size.X, Size.Y), cornerRadius.TopLeft, cornerRadius.TopRight, cornerRadius.BottomRight, cornerRadius.BottomLeft)); - base.RenderCore(canvas, currentTransformedClip, dirtyRects); + base.RenderCore(context, currentTransformedClip); } public override LtrbRect OwnContentBounds => new(0, 0, Size.X, Size.Y); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs index e38ae57c57..a494692021 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs @@ -5,9 +5,8 @@ namespace Avalonia.Rendering.Composition.Server; internal partial class ServerCompositionSolidColorVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { - canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new Rect(0, 0, Size.X, Size.Y)); + context.Canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new Rect(0, 0, Size.X, Size.Y)); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs index d4b1662bd9..28663ce342 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs @@ -5,8 +5,7 @@ namespace Avalonia.Rendering.Composition.Server; internal partial class ServerCompositionSurfaceVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { if (Surface == null) return; @@ -15,7 +14,7 @@ internal partial class ServerCompositionSurfaceVisual var bmp = Surface.Bitmap.Item; //TODO: add a way to always render the whole bitmap instead of just assuming 96 DPI - canvas.DrawBitmap(Surface.Bitmap.Item, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect( + context.Canvas.DrawBitmap(Surface.Bitmap.Item, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect( new Size(Size.X, Size.Y))); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs index d82502fc3c..1b5cac520e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs @@ -35,7 +35,7 @@ internal partial class ServerCompositionTarget public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(new(rect), Scaling).ToRect(); public LtrbRect SnapToDevicePixels(LtrbRect rect) => SnapToDevicePixels(rect, Scaling); - private static LtrbRect SnapToDevicePixels(LtrbRect rect, double scale) + public static LtrbRect SnapToDevicePixels(LtrbRect rect, double scale) { return new LtrbRect( Math.Floor(rect.Left * scale) / scale, diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index a39b3ae03f..8afdb6a2cc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -140,7 +140,6 @@ namespace Avalonia.Rendering.Composition.Server if (!_redrawRequested) return; - _redrawRequested = false; var renderTargetWithProperties = _renderTarget as IRenderTargetWithProperties; @@ -199,14 +198,14 @@ namespace Avalonia.Rendering.Composition.Server RenderedVisuals = 0; + _redrawRequested = false; DirtyRects.Reset(); } } void RenderRootToContextWithClip(IDrawingContextImpl context, ServerCompositionVisual root) { - var useLayerClip = Compositor.Options.UseSaveLayerRootClip ?? - Compositor.RenderInterface.GpuContext != null; + var useLayerClip = Compositor.Options.UseSaveLayerRootClip ?? false; using (DirtyRects.BeginDraw(context)) { @@ -215,7 +214,10 @@ namespace Avalonia.Rendering.Composition.Server context.PushLayer(DirtyRects.CombinedRect.ToRectUnscaled()); using (var proxy = new CompositorDrawingContextProxy(context)) - root.Render(proxy, null, DirtyRects); + { + var ctx = new ServerVisualRenderContext(proxy, DirtyRects, false); + root.Render(ctx, null); + } if (useLayerClip) context.PopLayer(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index ccc4b658be..54848d885d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -22,24 +22,22 @@ namespace Avalonia.Rendering.Composition.Server private LtrbRect? _transformedClipBounds; private LtrbRect _combinedTransformedClipBounds; - protected virtual void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected virtual void RenderCore(ServerVisualRenderContext canvas, LtrbRect currentTransformedClip) { } - public void Render(CompositorDrawingContextProxy canvas, LtrbRect? parentTransformedClip, IDirtyRectTracker dirtyRects) + public void Render(ServerVisualRenderContext context, LtrbRect? parentTransformedClip) { if (Visible == false || IsVisibleInFrame == false) return; if (Opacity == 0) return; - + var canvas = context.Canvas; + var currentTransformedClip = parentTransformedClip.HasValue ? parentTransformedClip.Value.Intersect(_combinedTransformedClipBounds) : _combinedTransformedClipBounds; - if (currentTransformedClip.IsZeroSize) - return; - if(!dirtyRects.Intersects(currentTransformedClip)) + if(!context.ShouldRender(this, currentTransformedClip)) return; Root!.RenderedVisuals++; @@ -49,12 +47,16 @@ namespace Avalonia.Rendering.Composition.Server if (AdornedVisual != null) { + // Adorners are currently not supported in detached rendering mode + if(context.DetachedRendering) + return; + canvas.Transform = Matrix.Identity; if (AdornerIsClipped) canvas.PushClip(AdornedVisual._combinedTransformedClipBounds.ToRect()); } - var transform = GlobalTransformMatrix; - canvas.Transform = transform; + + using var _ = context.SetOrPushTransform(this); var applyRenderOptions = RenderOptions != default; @@ -72,7 +74,7 @@ namespace Avalonia.Rendering.Composition.Server if (OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - RenderCore(canvas, currentTransformedClip, dirtyRects); + RenderCore(context, currentTransformedClip); if (OpacityMaskBrush != null) canvas.PopOpacityMask(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs index bbdea64004..7d203a7c47 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs @@ -47,4 +47,44 @@ internal partial class ServerCompositor lock (_renderInterfaceFeaturesUserApiLock) return _renderInterfaceFeatureCache ??= RenderInterface.Value.PublicFeatures; } + + public IBitmapImpl CreateCompositionVisualSnapshot(ServerCompositionVisual visual, + double scaling) + { + using (RenderInterface.EnsureCurrent()) + { + var pixelSize = PixelSize.FromSize(new Size(visual.Size.X, visual.Size.Y), scaling); + + var scaleTransform = Matrix.CreateScale(scaling, scaling); + var invertRootTransform = visual.CombinedTransformMatrix.Invert(); + + IDrawingContextLayerImpl? target = null; + try + { + target = RenderInterface.Value.CreateOffscreenRenderTarget(pixelSize, scaling); + using (var canvas = target.CreateDrawingContext(false)) + { + var proxy = new CompositorDrawingContextProxy(canvas) + { + PostTransform = invertRootTransform * scaleTransform, + Transform = Matrix.Identity + }; + var ctx = new ServerVisualRenderContext(proxy, null, true); + visual.Render(ctx, null); + } + + if (target is IDrawingContextLayerWithRenderContextAffinityImpl affined) + return affined.CreateNonAffinedSnapshot(); + + // We are returning the original target, so prevent it from being disposed + var rv = target; + target = null; + return rv; + } + finally + { + target?.Dispose(); + } + } + } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 196bd88409..dd540ecf9f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -23,6 +23,7 @@ namespace Avalonia.Rendering.Composition.Server private readonly Queue _batches = new Queue(); private readonly Queue _receivedJobQueue = new(); + private readonly Queue _receivedPostTargetJobQueue = new(); public long LastBatchId { get; private set; } public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } @@ -36,6 +37,8 @@ namespace Avalonia.Rendering.Composition.Server internal static readonly object RenderThreadDisposeStartMarker = new(); internal static readonly object RenderThreadJobsStartMarker = new(); internal static readonly object RenderThreadJobsEndMarker = new(); + internal static readonly object RenderThreadPostTargetJobsStartMarker = new(); + internal static readonly object RenderThreadPostTargetJobsEndMarker = new(); public CompositionOptions Options { get; } public ServerCompositorAnimations Animations { get; } @@ -83,7 +86,12 @@ namespace Avalonia.Rendering.Composition.Server var readObject = stream.ReadObject(); if (readObject == RenderThreadJobsStartMarker) { - ReadServerJobs(stream); + ReadServerJobs(stream, _receivedJobQueue, RenderThreadJobsEndMarker); + continue; + } + if (readObject == RenderThreadPostTargetJobsStartMarker) + { + ReadServerJobs(stream, _receivedPostTargetJobQueue, RenderThreadPostTargetJobsEndMarker); continue; } @@ -111,11 +119,11 @@ namespace Avalonia.Rendering.Composition.Server } } - void ReadServerJobs(BatchStreamReader reader) + void ReadServerJobs(BatchStreamReader reader, Queue queue, object endMarker) { object? readObject; - while ((readObject = reader.ReadObject()) != RenderThreadJobsEndMarker) - _receivedJobQueue.Enqueue((Action)readObject!); + while ((readObject = reader.ReadObject()) != endMarker) + queue.Enqueue((Action)readObject!); } void ReadDisposeJobs(BatchStreamReader reader) @@ -128,12 +136,12 @@ namespace Avalonia.Rendering.Composition.Server } } - void ExecuteServerJobs() + void ExecuteServerJobs(Queue queue) { - while(_receivedJobQueue.Count > 0) + while(queue.Count > 0) try { - _receivedJobQueue.Dequeue()(); + queue.Dequeue()(); } catch { @@ -218,10 +226,13 @@ namespace Avalonia.Rendering.Composition.Server try { + if(!RenderInterface.IsReady) + return; RenderInterface.EnsureValidBackendContext(); - ExecuteServerJobs(); + ExecuteServerJobs(_receivedJobQueue); foreach (var t in _activeTargets) t.Render(); + ExecuteServerJobs(_receivedPostTargetJobQueue); } catch (Exception e) when(RT_OnContextLostExceptionFilterObserver(e) && catchExceptions) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs index 203a530c9b..8ba5470e7f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs @@ -71,11 +71,10 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer Compositor.Animations.AddToClock(this); } - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext ctx, LtrbRect currentTransformedClip) { - canvas.AutoFlush = true; - using var context = new ImmediateDrawingContext(canvas, GlobalTransformMatrix, false); + ctx.Canvas.AutoFlush = true; + using var context = new ImmediateDrawingContext(ctx.Canvas, GlobalTransformMatrix, false); try { _handler.Render(context, currentTransformedClip.ToRect()); @@ -86,6 +85,6 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer ?.Log(_handler, $"Exception in {_handler.GetType().Name}.{nameof(CompositionCustomVisualHandler.OnRender)} {{0}}", e); } - canvas.AutoFlush = false; + ctx.Canvas.AutoFlush = false; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs new file mode 100644 index 0000000000..0778a7a621 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerVisualRenderContext +{ + public IDirtyRectTracker? DirtyRects { get; } + public bool DetachedRendering { get; } + public CompositorDrawingContextProxy Canvas { get; } + private readonly Stack? _transformStack; + + public ServerVisualRenderContext(CompositorDrawingContextProxy canvas, IDirtyRectTracker? dirtyRects, + bool detachedRendering) + { + Canvas = canvas; + DirtyRects = dirtyRects; + DetachedRendering = detachedRendering; + if (detachedRendering) + { + _transformStack = new(); + _transformStack.Push(canvas.Transform); + } + } + + + public bool ShouldRender(ServerCompositionVisual visual, LtrbRect currentTransformedClip) + { + if (DetachedRendering) + return true; + if (currentTransformedClip.IsZeroSize) + return false; + if (DirtyRects?.Intersects(currentTransformedClip) == false) + return false; + return true; + } + + public bool ShouldRenderOwnContent(ServerCompositionVisual visual, LtrbRect currentTransformedClip) + { + if (DetachedRendering) + return true; + return currentTransformedClip.Intersects(visual.TransformedOwnContentBounds) + && DirtyRects?.Intersects(visual.TransformedOwnContentBounds) != false; + } + + public RestoreTransform SetOrPushTransform(ServerCompositionVisual visual) + { + if (!DetachedRendering) + { + Canvas.Transform = visual.GlobalTransformMatrix; + return default; + } + else + { + var transform = visual.CombinedTransformMatrix * _transformStack!.Peek(); + Canvas.Transform = transform; + _transformStack.Push(transform); + return new RestoreTransform(this); + } + } + + public struct RestoreTransform(ServerVisualRenderContext? parent) : IDisposable + { + public void Dispose() + { + if (parent != null) + { + parent._transformStack!.Pop(); + parent.Canvas.Transform = parent._transformStack.Peek(); + } + } + } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index d6576511b9..db61ad84f1 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -11,16 +11,24 @@ internal class PlatformRenderInterfaceContextManager private readonly IPlatformGraphics? _graphics; private IPlatformRenderInterfaceContext? _backend; private OwnedDisposable? _gpuContext; + private readonly IPlatformGraphicsReadyStateFeature? _readyStateFeature; public event Action? ContextDisposed; public event Action? ContextCreated; public PlatformRenderInterfaceContextManager(IPlatformGraphics? graphics) { _graphics = graphics; + _readyStateFeature = (_graphics as IPlatformGraphicsWithFeatures) + ?.TryGetFeature(); } + public bool IsReady => _readyStateFeature?.IsReady ?? true; + public void EnsureValidBackendContext() { + if (!IsReady) + throw new InvalidOperationException("Platform graphics isn't ready yet"); + if (_backend == null || _gpuContext?.Value.IsLost == true) { _backend?.Dispose(); @@ -34,10 +42,14 @@ internal class PlatformRenderInterfaceContextManager if (_graphics != null) { - if (_graphics.UsesSharedContext) - _gpuContext = new OwnedDisposable(_graphics.GetSharedContext(), false); - else - _gpuContext = new OwnedDisposable(_graphics.CreateContext(), true); + if (_readyStateFeature?.UsesContexts != false) + { + if (_graphics.UsesSharedContext) + _gpuContext = + new OwnedDisposable(_graphics.GetSharedContext(), false); + else + _gpuContext = new OwnedDisposable(_graphics.CreateContext(), true); + } } _backend = AvaloniaLocator.Current.GetRequiredService() diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 3a0f0a4763..8224aea99f 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -420,19 +420,6 @@ namespace Avalonia } } - internal StyleDiagnostics GetStyleDiagnosticsInternal() - { - var styles = new List(); - - foreach (var frame in GetValueStore().Frames) - { - if (frame is IStyleInstance style) - styles.Add(new(style)); - } - - return new StyleDiagnostics(styles); - } - /// void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { diff --git a/src/Avalonia.Base/Utilities/SafeEnumerableHashSet.cs b/src/Avalonia.Base/Utilities/SafeEnumerableHashSet.cs new file mode 100644 index 0000000000..07d56c8f0f --- /dev/null +++ b/src/Avalonia.Base/Utilities/SafeEnumerableHashSet.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + /// + /// Implements a simple set which is safe to modify during enumeration. + /// + /// The item type. + /// + /// Implements a set which, when written to while enumerating, performs a copy of the set + /// items. Note this class doesn't actually implement as it's not + /// currently needed - feel free to add missing methods etc. + /// + internal class SafeEnumerableHashSet : IEnumerable + { + private HashSet _hashSet = new(); + private int _generation; + private int _enumCount = 0; + + public int Count => _hashSet.Count; + internal HashSet Inner => _hashSet; + + public void Add(T item) => GetSet().Add(item); + public bool Remove(T item) => GetSet().Remove(item); + + public Enumerator GetEnumerator() => new(this, _hashSet); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private HashSet GetSet() + { + if (_enumCount > 0) + { + // .NET has a fastpath for cloning a hashset when passed in via the constructor + _hashSet = new(_hashSet); + ++_generation; + _enumCount = 0; + } + + return _hashSet; + } + + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly SafeEnumerableHashSet _owner; + private readonly int _generation; + private HashSet.Enumerator _enumerator; + + internal Enumerator(SafeEnumerableHashSet owner, HashSet list) + { + _owner = owner; + _generation = owner._generation; + ++_owner._enumCount; + _enumerator = list.GetEnumerator(); + } + + public void Dispose() + { + _enumerator.Dispose(); + if (_owner._generation == _generation) + --_owner._enumCount; + } + + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + public T Current => _enumerator.Current; + object? IEnumerator.Current => _enumerator.Current; + + void IEnumerator.Reset() + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/Avalonia.Base/Utilities/SafeEnumerableList.cs b/src/Avalonia.Base/Utilities/SafeEnumerableList.cs deleted file mode 100644 index dd437d27be..0000000000 --- a/src/Avalonia.Base/Utilities/SafeEnumerableList.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.Collections; -using System.Collections.Generic; - -namespace Avalonia.Utilities -{ - /// - /// Implements a simple list which is safe to modify during enumeration. - /// - /// The item type. - /// - /// Implements a list which, when written to while enumerating, performs a copy of the list - /// items. Note this this class doesn't actually implement as it's not - /// currently needed - feel free to add missing methods etc. - /// - internal class SafeEnumerableList : IEnumerable - { - private List _list = new(); - private int _generation; - private int _enumCount = 0; - - public int Count => _list.Count; - internal List Inner => _list; - - public void Add(T item) => GetList().Add(item); - public bool Remove(T item) => GetList().Remove(item); - - public Enumerator GetEnumerator() => new(this, _list); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - private List GetList() - { - if (_enumCount > 0) - { - _list = new(_list); - ++_generation; - _enumCount = 0; - } - - return _list; - } - - public struct Enumerator : IEnumerator, IEnumerator - { - private readonly SafeEnumerableList _owner; - private readonly List _list; - private readonly int _generation; - private int _index; - private T? _current; - - internal Enumerator(SafeEnumerableList owner, List list) - { - _owner = owner; - _list = list; - _generation = owner._generation; - _index = 0; - _current = default; - ++_owner._enumCount; - } - - public void Dispose() - { - if (_owner._generation == _generation) - --_owner._enumCount; - } - - public bool MoveNext() - { - if (_index < _list.Count) - { - _current = _list[_index++]; - return true; - } - - _current = default; - return false; - } - - public T Current => _current!; - object? IEnumerator.Current => _current; - - void IEnumerator.Reset() - { - _index = 0; - _current = default; - } - } - } -} diff --git a/src/Avalonia.Base/Utilities/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs index a78c7988f7..ea6769c1ba 100644 --- a/src/Avalonia.Base/Utilities/SmallDictionary.cs +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -127,6 +127,18 @@ internal struct InlineDictionary : IEnumerable dic) + dic.Clear(); + else + _data = null; + } + public bool HasEntries => _data != null; public bool TryGetValue(TKey key, [MaybeNullWhen(false)]out TValue value) diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 2274833cf2..2caaf7fc5e 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -1,6 +1,7 @@  - netstandard2.0 + netstandard2.0 + enable false tools $(DefineConstants);BUILDTASK;XAMLX_CECIL_INTERNAL;XAMLX_INTERNAL @@ -123,16 +124,18 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) - - - - - - - + + + + + + + + - + + diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index 8cd444d30b..7ad2ffba5d 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -38,7 +38,7 @@ namespace Avalonia.Build.Tasks { // To simplify incremental build checks, copy the input files to the expected output locations even if the Xaml compiler didn't do anything. CopyAndTouch(AssemblyFile.ItemSpec, outputPath); - CopyAndTouch(Path.ChangeExtension(AssemblyFile.ItemSpec, ".pdb"), Path.ChangeExtension(outputPath, ".pdb")); + CopyAndTouch(Path.ChangeExtension(AssemblyFile.ItemSpec, ".pdb"), Path.ChangeExtension(outputPath, ".pdb"), false); if (!string.IsNullOrEmpty(refOutputPath)) { @@ -49,8 +49,18 @@ namespace Avalonia.Build.Tasks return res.Success; } - private static void CopyAndTouch(string source, string destination) + private static void CopyAndTouch(string source, string destination, bool shouldExist = true) { + if (!File.Exists(source)) + { + if (shouldExist) + { + throw new FileNotFoundException($"Could not copy file '{source}'. File does not exist."); + } + + return; + } + File.Copy(source, destination, overwrite: true); File.SetLastWriteTimeUtc(destination, DateTime.UtcNow); } diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index dd11dd7003..3f9f36d130 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -115,22 +115,22 @@ namespace Avalonia.Build.Tasks // documentation, on not windows platform Debugger.Launch() always return true without running a debugger. if (System.Diagnostics.Debugger.Launch()) { - // Set timeout at 1 minut. + // Set timeout at 1 minute. var time = new System.Diagnostics.Stopwatch(); var timeout = TimeSpan.FromMinutes(1); time.Start(); - // wait for the debugger to be attacked or timeout. + // wait for the debugger to be attached or timeout. while (!System.Diagnostics.Debugger.IsAttached && time.Elapsed < timeout) { - engine.LogMessage($"[PID:{System.Diagnostics.Process.GetCurrentProcess().Id}] Wating attach debugger. Elapsed {time.Elapsed}...", MessageImportance.High); + engine.LogMessage($"[PID:{System.Diagnostics.Process.GetCurrentProcess().Id}] Waiting to attach debugger. Elapsed {time.Elapsed}...", MessageImportance.High); System.Threading.Thread.Sleep(100); } time.Stop(); if (time.Elapsed >= timeout) { - engine.LogMessage("Wating attach debugger timeout.", MessageImportance.Normal); + engine.LogMessage("Waiting to attach debugger has timed out.", MessageImportance.Normal); } } else diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj index ed080e3065..db7bc33232 100644 --- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -4,7 +4,7 @@ true - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml index 102d208ed1..c2535086fa 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml @@ -103,7 +103,8 @@ - + @@ -168,7 +169,8 @@ - + @@ -203,7 +205,8 @@ - + @@ -264,6 +267,8 @@ AutomationProperties.Name="Hexadecimal Color" Height="32" MaxLength="9" + Padding="10,6,6,5" + VerticalContentAlignment="Center" HorizontalAlignment="Stretch" CornerRadius="0,0,0,0" /> diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml index 725c94b10f..942d4e5a03 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml @@ -306,7 +306,8 @@ - + @@ -371,7 +372,8 @@ - + @@ -406,7 +408,8 @@ - + @@ -467,6 +470,8 @@ AutomationProperties.Name="Hexadecimal Color" Height="32" MaxLength="9" + Padding="10,6,6,5" + VerticalContentAlignment="Center" HorizontalAlignment="Stretch" CornerRadius="0,0,0,0" /> diff --git a/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt b/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt deleted file mode 100644 index bfcec2960a..0000000000 --- a/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt +++ /dev/null @@ -1,4 +0,0 @@ -Compat issues with assembly Avalonia.Controls.DataGrid: -MembersMustExist : Member 'protected void Avalonia.Controls.DataGridCheckBoxColumn.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.DataGridTextColumn.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -Total Issues: 2 diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index c3f428450b..c314d71d49 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -1183,7 +1183,7 @@ namespace Avalonia.Controls row.EnsureHeaderStyleAndVisibility(null); if (newValueRows) { - row.UpdatePseudoClasses(); + row.ApplyState(); row.EnsureHeaderVisibility(); } } @@ -1720,7 +1720,7 @@ namespace Avalonia.Controls // State for the old row needs to be applied after setting the new value if (oldMouseOverRow != null) { - oldMouseOverRow.UpdatePseudoClasses(); + oldMouseOverRow.ApplyState(); } if (_mouseOverRowIndex.HasValue) @@ -1732,7 +1732,7 @@ namespace Avalonia.Controls Debug.Assert(newMouseOverRow != null); if (newMouseOverRow != null) { - newMouseOverRow.UpdatePseudoClasses(); + newMouseOverRow.ApplyState(); } } } @@ -2140,7 +2140,7 @@ namespace Avalonia.Controls if (DataConnection.DataSource != null && !DataConnection.EventsWired) { DataConnection.WireEvents(DataConnection.DataSource); - InitializeElements(false /*recycleRows*/); + InitializeElements(true /*recycleRows*/); } } @@ -4177,7 +4177,7 @@ namespace Avalonia.Controls if (editingRow.IsValid) { editingRow.IsValid = false; - editingRow.UpdatePseudoClasses(); + editingRow.ApplyState(); } } @@ -4368,7 +4368,7 @@ namespace Avalonia.Controls //IsTabStop = true; if (IsSlotVisible(EditingRow.Slot)) { - EditingRow.UpdatePseudoClasses(); + EditingRow.ApplyState(); } ResetEditingRow(); if (keepFocus) @@ -6224,7 +6224,7 @@ namespace Avalonia.Controls cell.UpdatePseudoClasses(); } } - EditingRow.UpdatePseudoClasses(); + EditingRow.ApplyState(); } } IsValid = true; diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 852b85ff01..3518738485 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Controls.Automation.Peers; using Avalonia.Controls.Metadata; @@ -37,6 +38,7 @@ namespace Avalonia.Controls (x,e) => x.DataGridCell_PointerPressed(e), handledEventsToo: true); FocusableProperty.OverrideDefaultValue(true); IsTabStopProperty.OverrideDefaultValue(false); + AutomationProperties.IsOffscreenBehaviorProperty.OverrideDefaultValue(IsOffscreenBehavior.FromClip); } public DataGridCell() { } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs index 35f400c208..510ff8561d 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs @@ -12,7 +12,6 @@ namespace Avalonia.Controls { internal class DataGridColumnCollection : ObservableCollection { - private readonly Dictionary _columnsMap = new Dictionary(); private readonly DataGrid _owningGrid; public DataGridColumnCollection(DataGrid owningGrid) @@ -280,12 +279,10 @@ namespace Avalonia.Controls VisibleStarColumnCount = 0; VisibleEdgedColumnsWidth = 0; VisibleColumnCount = 0; - _columnsMap.Clear(); for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++) { var item = ItemsInternal[columnIndex]; - _columnsMap[columnIndex] = item.DisplayIndex; if (item.IsVisible) { VisibleColumnCount++; @@ -299,11 +296,6 @@ namespace Avalonia.Controls } } - internal int GetColumnDisplayIndex(int columnIndex) - { - return _columnsMap.TryGetValue(columnIndex, out var displayIndex) ? displayIndex : -1; - } - internal DataGridColumn GetColumnAtDisplayIndex(int displayIndex) { if (displayIndex < 0 || displayIndex >= ItemsInternal.Count || displayIndex >= DisplayIndexMap.Count) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 579c4d04ad..a3dcc433ee 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -6,6 +6,7 @@ using System; using System.ComponentModel; using System.Diagnostics; +using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Collections; using Avalonia.Controls.Automation.Peers; @@ -74,6 +75,7 @@ namespace Avalonia.Controls AreSeparatorsVisibleProperty.Changed.AddClassHandler((x, e) => x.OnAreSeparatorsVisibleChanged(e)); PressedMixin.Attach(); IsTabStopProperty.OverrideDefaultValue(false); + AutomationProperties.IsOffscreenBehaviorProperty.OverrideDefaultValue(IsOffscreenBehavior.FromClip); } /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 5a312e8b7c..3af85223a8 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -17,6 +17,7 @@ using System; using System.Diagnostics; using Avalonia.Automation.Peers; using Avalonia.Reactive; +using Avalonia.Automation; namespace Avalonia.Controls { @@ -63,6 +64,7 @@ namespace Avalonia.Controls private Control _detailsContent; private IDisposable _detailsContentSizeSubscription; private DataGridDetailsPresenter _detailsElement; + private bool _isSelected; // Locally cache whether or not details are visible so we don't run redundant storyboards // The Details Template that is actually applied to the Row @@ -85,6 +87,18 @@ namespace Avalonia.Controls set { SetValue(HeaderProperty, value); } } + public static readonly DirectProperty IsSelectedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsSelected), + o => o.IsSelected, + (o, v) => o.IsSelected = v); + + public bool IsSelected + { + get => _isSelected; + set => SetAndRaise(IsSelectedProperty, ref _isSelected, value); + } + public static readonly DirectProperty IsValidProperty = AvaloniaProperty.RegisterDirect( nameof(IsValid), @@ -130,6 +144,7 @@ namespace Avalonia.Controls AreDetailsVisibleProperty.Changed.AddClassHandler((x, e) => x.OnAreDetailsVisibleChanged(e)); PointerPressedEvent.AddClassHandler((x, e) => x.DataGridRow_PointerPressed(e), handledEventsToo: true); IsTabStopProperty.OverrideDefaultValue(false); + AutomationProperties.IsOffscreenBehaviorProperty.OverrideDefaultValue(IsOffscreenBehavior.FromClip); } /// @@ -220,13 +235,18 @@ namespace Avalonia.Controls set; } + private int _index; + + public static readonly DirectProperty IndexProperty = AvaloniaProperty.RegisterDirect( + nameof(Index), o => o.Index, (o, v) => o.Index = v); + /// /// Index of the row /// - internal int Index + public int Index { - get; - set; + get => _index; + internal set => SetAndRaise(IndexProperty, ref _index, value); } internal double ActualBottomGridLineHeight @@ -347,20 +367,6 @@ namespace Avalonia.Controls } } - internal bool IsSelected - { - get - { - if (OwningGrid == null || Slot == -1) - { - // The Slot can be -1 if we're about to reuse or recycle this row, but the layout cycle has not - // passed so we don't know the outcome yet. We don't care whether or not it's selected in this case - return false; - } - return OwningGrid.GetRowSelection(Slot); - } - } - internal int? MouseOverColumnIndex { get @@ -435,6 +441,7 @@ namespace Avalonia.Controls /// /// The index of the current row. /// + [Obsolete("This API is going to be removed in a future version. Use the Index property instead.")] public int GetIndex() { return Index; @@ -558,7 +565,7 @@ namespace Avalonia.Controls RootElement = e.NameScope.Find(DATAGRIDROW_elementRoot); if (RootElement != null) { - UpdatePseudoClasses(); + ApplyState(); } bool updateVerticalScrollBar = false; @@ -644,11 +651,12 @@ namespace Avalonia.Controls } } - internal void UpdatePseudoClasses() + internal void ApplyState() { if (RootElement != null && OwningGrid != null && IsVisible) { - PseudoClasses.Set(":selected", IsSelected); + var isSelected = Slot != -1 && OwningGrid.GetRowSelection(Slot); + IsSelected = isSelected; PseudoClasses.Set(":editing", IsEditing); PseudoClasses.Set(":invalid", !IsValid); ApplyHeaderStatus(); @@ -1061,7 +1069,6 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == DataContextProperty) @@ -1080,6 +1087,18 @@ namespace Avalonia.Controls } } } + else if (change.Property == IsSelectedProperty) + { + var value = change.GetNewValue(); + + if (OwningGrid != null && Slot != -1) + { + OwningGrid.SetRowSelection(Slot, value, false); + } + + PseudoClasses.Set(":selected", value); + } + base.OnPropertyChanged(change); } diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index a480a7af15..ba6ad94777 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -3,6 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.Automation; using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Media; @@ -50,6 +51,11 @@ namespace Avalonia.Controls.Primitives AddHandler(PointerPressedEvent, DataGridRowHeader_PointerPressed, handledEventsToo: true); } + static DataGridRowHeader() + { + AutomationProperties.IsOffscreenBehaviorProperty.OverrideDefaultValue(IsOffscreenBehavior.FromClip); + } + internal Control Owner { get; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index c9a348d172..15a14a61c2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -677,7 +677,7 @@ namespace Avalonia.Controls { if (DisplayData.GetDisplayedElement(slot) is DataGridRow row) { - row.UpdatePseudoClasses(); ; + row.ApplyState(); } slot = GetNextVisibleSlot(slot); } @@ -1513,7 +1513,7 @@ namespace Avalonia.Controls if (row.IsSelected || row.IsRecycled) { - row.UpdatePseudoClasses(); + row.ApplyState(); } // Show or hide RowDetails based on DataGrid settings @@ -1927,7 +1927,7 @@ namespace Avalonia.Controls Control element = DisplayData.GetDisplayedElement(slot); if (element is DataGridRow row) { - row.UpdatePseudoClasses(); + row.ApplyState(); EnsureRowDetailsVisibility(row, raiseNotification: true, animate: true); } else diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index f5db7c0855..072ad6d078 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -53,13 +53,13 @@ namespace Avalonia.Controls.Primitives int IChildIndexProvider.GetChildIndex(ILogical child) { return child is DataGridCell cell - ? OwningGrid.ColumnsInternal.GetColumnDisplayIndex(cell.ColumnIndex) + ? cell.OwningColumn?.DisplayIndex ?? -1 : throw new InvalidOperationException("Invalid cell type"); } bool IChildIndexProvider.TryGetTotalCount(out int count) { - count = OwningGrid.ColumnsInternal.VisibleColumnCount; + count = Children.Count - 1; // Adjust for filler column return true; } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index fb7b256275..52052a127d 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -118,13 +118,13 @@ namespace Avalonia.Controls.Primitives int IChildIndexProvider.GetChildIndex(ILogical child) { return child is DataGridColumnHeader header - ? OwningGrid.ColumnsInternal.GetColumnDisplayIndex(header.ColumnIndex) + ? header.OwningColumn?.DisplayIndex ?? -1 : throw new InvalidOperationException("Invalid cell type"); } bool IChildIndexProvider.TryGetTotalCount(out int count) { - count = OwningGrid.ColumnsInternal.VisibleColumnCount; + count = Children.Count - 1; // Adjust for filler column return true; } diff --git a/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs b/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs index 38e18b57de..a33ae0e80b 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs @@ -19,6 +19,8 @@ namespace Avalonia.Controls.Utils internal const char LeftIndexerToken = '['; internal const char PropertyNameSeparator = '.'; internal const char RightIndexerToken = ']'; + internal const char LeftParenthesisToken = '('; + internal const char RightParenthesisToken = ')'; private static Type FindGenericType(Type definition, Type type) { @@ -482,12 +484,35 @@ namespace Avalonia.Controls.Utils List propertyPaths = new List(); if (!string.IsNullOrEmpty(propertyPath)) { + bool parenthesisOn = false; int startIndex = 0; for (int index = 0; index < propertyPath.Length; index++) { - if (propertyPath[index] == PropertyNameSeparator) + if (parenthesisOn) { - propertyPaths.Add(propertyPath.Substring(startIndex, index - startIndex)); + if (propertyPath[index] == RightParenthesisToken) + { + parenthesisOn = false; + startIndex = index + 1; + } + continue; + } + + if (propertyPath[index] == LeftParenthesisToken) + { + parenthesisOn = true; + if (startIndex != index) + { + propertyPaths.Add(propertyPath.Substring(startIndex, index - startIndex)); + startIndex = index + 1; + } + } + else if (propertyPath[index] == PropertyNameSeparator) + { + if (startIndex != index) + { + propertyPaths.Add(propertyPath.Substring(startIndex, index - startIndex)); + } startIndex = index + 1; } else if (startIndex != index && propertyPath[index] == LeftIndexerToken) diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt deleted file mode 100644 index 33e5efbc15..0000000000 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ /dev/null @@ -1,112 +0,0 @@ -Compat issues with assembly Avalonia.Controls: -MembersMustExist : Member 'protected void Avalonia.Controls.Button.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.ButtonSpinner.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.CalendarDatePicker.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.ContextMenu.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -TypesMustExist : Type 'Avalonia.Controls.DropDown' does not exist in the implementation but it does exist in the contract. -TypesMustExist : Type 'Avalonia.Controls.DropDownItem' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.Expander.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick.get()' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.ItemsRepeater.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.DirectProperty Avalonia.DirectProperty Avalonia.Controls.NumericUpDown.ValueProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.IncrementProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.MaximumProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.NumericUpDown.MinimumProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Increment.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Increment.set(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Maximum.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Maximum.set(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Minimum.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Minimum.set(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceIncrement(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceMaximum(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceMinimum(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected System.Double Avalonia.Controls.NumericUpDown.OnCoerceValue(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnIncrementChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnMaximumChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnMinimumChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.OnValueChanged(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.NumericUpDown.RaiseValueChangedEvent(System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDown.Value.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Value.set(System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChangedEventArgs..ctor(Avalonia.Interactivity.RoutedEvent, System.Double, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.ProgressBar.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.Slider.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontFamilyProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontStyleProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontWeightProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.ForegroundProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontSizeProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextAlignmentProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextTrimmingProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextWrappingProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.LineHeightProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.MaxLinesProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Media.FontFamily Avalonia.Controls.TextBlock.GetFontFamily(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Double Avalonia.Controls.TextBlock.GetFontSize(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Media.FontStyle Avalonia.Controls.TextBlock.GetFontStyle(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Media.FontWeight Avalonia.Controls.TextBlock.GetFontWeight(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Media.IBrush Avalonia.Controls.TextBlock.GetForeground(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontFamily(Avalonia.Controls.Control, Avalonia.Media.FontFamily)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontSize(Avalonia.Controls.Control, System.Double)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontStyle(Avalonia.Controls.Control, Avalonia.Media.FontStyle)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontWeight(Avalonia.Controls.Control, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetForeground(Avalonia.Controls.Control, Avalonia.Media.IBrush)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.TextBox.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.TopLevel' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. -CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Viewbox' does not inherit from base type 'Avalonia.Controls.Decorator' in the implementation but it does in the contract. -MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. -CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Window' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.Window.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.TryShutdown(System.Int32)' is present in the implementation but not in the contract. -CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. -MembersMustExist : Member 'public System.Action Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.Notifications.WindowNotificationManager.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. -MembersMustExist : Member 'protected void Avalonia.Controls.Presenters.ScrollContentPresenter.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.CreateFormattedText()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.FormattedText.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public System.Int32 Avalonia.Controls.Presenters.TextPresenter.GetCaretIndex(Avalonia.Point)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.Presenters.TextPresenter.InvalidateFormattedText()' does not exist in the implementation but it does exist in the contract. -CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber' in the implementation but it does in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.ScrollBar.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.Track.OnPropertyChanged(Avalonia.AvaloniaPropertyChangedEventArgs)' does not exist in the implementation but it does exist in the contract. -TypesMustExist : Type 'Avalonia.Platform.ExportWindowingSubsystemAttribute' does not exist in the implementation but it does exist in the contract. -EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.Screen Avalonia.Platform.IScreenImpl.ScreenFromPoint(Avalonia.PixelPoint)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.Screen Avalonia.Platform.IScreenImpl.ScreenFromRect(Avalonia.PixelRect)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.Screen Avalonia.Platform.IScreenImpl.ScreenFromWindow(Avalonia.Platform.IWindowBaseImpl)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the contract but not in the implementation. -MembersMustExist : Member 'public System.Action Avalonia.Platform.ITopLevelImpl.Resized.get()' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' is present in the contract but not in the implementation. -MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. -MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' is present in the contract but not in the implementation. -MembersMustExist : Member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean, System.Boolean)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation. -MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 110 diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 43c140a05f..2957214308 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -117,16 +117,30 @@ namespace Avalonia.Controls.ApplicationLifetimes } public int Start(string[] args) + { + return StartCore(args); + } + + /// + /// Since the lifetime must be set up/prepared with 'args' before executing Start(), an overload with no parameters seems more suitable for integrating with some lifetime manager providers, such as MS HostApplicationBuilder. + /// + /// exit code + public int Start() + { + return StartCore(Args ?? Array.Empty()); + } + + internal int StartCore(string[] args) { SetupCore(args); - + _cts = new CancellationTokenSource(); // Note due to a bug in the JIT we wrap this in a method, otherwise MainWindow // gets stuffed into a local var and can not be GCed until after the program stops. // this method never exits until program end. - ShowMainWindow(); - + ShowMainWindow(); + Dispatcher.UIThread.MainLoop(_cts.Token); Environment.ExitCode = _exitCode; return _exitCode; diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 35d6c13918..bda9a91183 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -153,6 +153,17 @@ namespace Avalonia.Automation.Peers /// public bool IsKeyboardFocusable() => IsKeyboardFocusableCore(); + /// + /// Gets a value that indicates whether an element is off the screen. + /// + /// + /// This property does not indicate whether the element is visible. In some circumstances, + /// an element is on the screen but is still not visible. For example, if the element is + /// on the screen but obscured by other elements, it might not be visible. In this case, + /// the method returns false. + /// + public bool IsOffscreen() => IsOffscreenCore(); + /// /// Sets the keyboard focus on the element that is associated with this automation peer. /// @@ -245,6 +256,7 @@ namespace Avalonia.Automation.Peers protected abstract bool IsControlElementCore(); protected abstract bool IsEnabledCore(); protected abstract bool IsKeyboardFocusableCore(); + protected virtual bool IsOffscreenCore() => false; protected abstract void SetFocusCore(); protected abstract bool ShowContextMenuCore(); diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index b0dd30a878..0c043e7577 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Automation.Peers @@ -201,6 +202,19 @@ namespace Avalonia.Automation.Peers return view == AccessibilityView.Default ? IsControlElementCore() : view >= AccessibilityView.Control; } + protected override bool IsOffscreenCore() + { + return AutomationProperties.GetIsOffscreenBehavior(Owner) switch + { + IsOffscreenBehavior.Onscreen => false, + IsOffscreenBehavior.Offscreen => true, + IsOffscreenBehavior.FromClip => Owner.GetTransformedBounds() is not { } bounds || + MathUtilities.IsZero(bounds.Clip.Width) || + MathUtilities.IsZero(bounds.Clip.Height), + _ => !Owner.IsVisible, + }; + } + private static Rect GetBounds(Control control) { var root = control.GetVisualRoot(); diff --git a/src/Avalonia.Controls/Automation/Peers/InteropAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/InteropAutomationPeer.cs new file mode 100644 index 0000000000..367c7804c3 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/InteropAutomationPeer.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Peers; +using Avalonia.Platform; + +namespace Avalonia.Controls.Automation.Peers; + +/// +/// Represents the root of a native control automation tree hosted by a . +/// +/// +/// This peer should be special-cased in the platform backend, as it represents a native control +/// and hence none of the standard automation peer methods are applicable. +/// +internal class InteropAutomationPeer : AutomationPeer +{ + private AutomationPeer? _parent; + + public InteropAutomationPeer(IPlatformHandle nativeControlHandle) => NativeControlHandle = nativeControlHandle; + public IPlatformHandle NativeControlHandle { get; } + + protected override void BringIntoViewCore() => throw new NotImplementedException(); + protected override string? GetAcceleratorKeyCore() => throw new NotImplementedException(); + protected override string? GetAccessKeyCore() => throw new NotImplementedException(); + protected override AutomationControlType GetAutomationControlTypeCore() => throw new NotImplementedException(); + protected override string? GetAutomationIdCore() => throw new NotImplementedException(); + protected override Rect GetBoundingRectangleCore() => throw new NotImplementedException(); + protected override string GetClassNameCore() => throw new NotImplementedException(); + protected override AutomationPeer? GetLabeledByCore() => throw new NotImplementedException(); + protected override string? GetNameCore() => throw new NotImplementedException(); + protected override IReadOnlyList GetOrCreateChildrenCore() => throw new NotImplementedException(); + protected override AutomationPeer? GetParentCore() => _parent; + protected override bool HasKeyboardFocusCore() => throw new NotImplementedException(); + protected override bool IsContentElementCore() => throw new NotImplementedException(); + protected override bool IsControlElementCore() => throw new NotImplementedException(); + protected override bool IsEnabledCore() => throw new NotImplementedException(); + protected override bool IsKeyboardFocusableCore() => throw new NotImplementedException(); + protected override void SetFocusCore() => throw new NotImplementedException(); + protected override bool ShowContextMenuCore() => throw new NotImplementedException(); + + protected internal override bool TrySetParent(AutomationPeer? parent) + { + _parent = parent; + return true; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/NativeControlHostPeer.cs b/src/Avalonia.Controls/Automation/Peers/NativeControlHostPeer.cs new file mode 100644 index 0000000000..5f3ea30cb6 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/NativeControlHostPeer.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using Avalonia.Automation.Peers; + +namespace Avalonia.Controls.Automation.Peers; + +internal class NativeControlHostPeer : ControlAutomationPeer +{ + public NativeControlHostPeer(NativeControlHost owner) + : base(owner) + { + owner.NativeControlHandleChanged += OnNativeControlHandleChanged; + } + + protected override IReadOnlyList? GetChildrenCore() + { + if (Owner is NativeControlHost host && host.NativeControlHandle != null) + return [new InteropAutomationPeer(host.NativeControlHandle)]; + return null; + } + + private void OnNativeControlHandleChanged(object? sender, EventArgs e) => InvalidateChildren(); +} diff --git a/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs new file mode 100644 index 0000000000..e6920c1e02 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs @@ -0,0 +1,26 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Chrome; + +namespace Avalonia.Controls.Automation.Peers; + +internal class TitleBarAutomationPeer : ControlAutomationPeer +{ + public TitleBarAutomationPeer(TitleBar owner) : base(owner) + { + } + + protected override bool IsContentElementCore() => true; + + protected override string GetClassNameCore() + { + return "TitleBar"; + } + + protected override string? GetAutomationIdCore() => base.GetAutomationIdCore() ?? "AvaloniaTitleBar"; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.TitleBar; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TreeViewAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TreeViewAutomationPeer.cs new file mode 100644 index 0000000000..5bb0dcaa55 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TreeViewAutomationPeer.cs @@ -0,0 +1,16 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class TreeViewAutomationPeer : ItemsControlAutomationPeer +{ + public TreeViewAutomationPeer(TreeView owner) + : base(owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Tree; + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/TreeViewItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TreeViewItemAutomationPeer.cs new file mode 100644 index 0000000000..9629a75267 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/TreeViewItemAutomationPeer.cs @@ -0,0 +1,16 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers; + +public class TreeViewItemAutomationPeer : ItemsControlAutomationPeer +{ + public TreeViewItemAutomationPeer(TreeViewItem owner) + : base(owner) + { + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.TreeItem; + } +} diff --git a/src/Avalonia.Controls/BorderVisual.cs b/src/Avalonia.Controls/BorderVisual.cs index 36a557d5ed..3046e7e875 100644 --- a/src/Avalonia.Controls/BorderVisual.cs +++ b/src/Avalonia.Controls/BorderVisual.cs @@ -46,9 +46,9 @@ class CompositionBorderVisual : CompositionDrawListVisual { } - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext ctx, LtrbRect currentTransformedClip) { + var canvas = ctx.Canvas; if (ClipToBounds) { var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y))); @@ -58,7 +58,7 @@ class CompositionBorderVisual : CompositionDrawListVisual canvas.PushClip(new RoundedRect(clipRect, _cornerRadius)); } - base.RenderCore(canvas, currentTransformedClip, dirtyRects); + base.RenderCore(ctx, currentTransformedClip); if(ClipToBounds) canvas.PopClip(); diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index d8c31fc477..614847ee4e 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -34,8 +34,10 @@ namespace Avalonia.Controls [PseudoClasses(pcFlyoutOpen, pcPressed)] public class Button : ContentControl, ICommandSource, IClickableControl { - private const string pcPressed = ":pressed"; + private const string pcPressed = ":pressed"; private const string pcFlyoutOpen = ":flyout-open"; + private EventHandler? _canExecuteChangeHandler = default; + private EventHandler CanExecuteChangedHandler => _canExecuteChangeHandler ??= new(CanExecuteChanged); /// /// Defines the property. @@ -250,10 +252,11 @@ namespace Avalonia.Controls base.OnAttachedToLogicalTree(e); - if (Command != null) + (var command, var parameter) = (Command, CommandParameter); + if (command is not null) { - Command.CanExecuteChanged += CanExecuteChanged; - CanExecuteChanged(this, EventArgs.Empty); + command.CanExecuteChanged += CanExecuteChangedHandler; + CanExecuteChanged(command, parameter); } } @@ -269,9 +272,9 @@ namespace Avalonia.Controls base.OnDetachedFromLogicalTree(e); - if (Command != null) + if (Command is { } command) { - Command.CanExecuteChanged -= CanExecuteChanged; + command.CanExecuteChanged -= CanExecuteChangedHandler; } } @@ -286,8 +289,9 @@ namespace Avalonia.Controls OnClick(); e.Handled = true; break; - case Key.Space: + // Avoid handling Space if the button isn't focused: a child TextBox might need it for text input + if (IsFocused) { if (ClickMode == ClickMode.Press) { @@ -296,22 +300,21 @@ namespace Avalonia.Controls IsPressed = true; e.Handled = true; - break; } - + break; case Key.Escape when Flyout != null: // If Flyout doesn't have focusable content, close the flyout here CloseFlyout(); break; } - base.OnKeyDown(e); } /// protected override void OnKeyUp(KeyEventArgs e) { - if (e.Key == Key.Space) + // Avoid handling Space if the button isn't focused: a child TextBox might need it for text input + if (e.Key == Key.Space && IsFocused) { if (ClickMode == ClickMode.Release) { @@ -343,9 +346,10 @@ namespace Avalonia.Controls var e = new RoutedEventArgs(ClickEvent); RaiseEvent(e); - if (!e.Handled && Command?.CanExecute(CommandParameter) == true) + (var command, var parameter) = (Command, CommandParameter); + if (!e.Handled && command is not null && command.CanExecute(parameter)) { - Command.Execute(CommandParameter); + command.Execute(parameter); e.Handled = true; } } @@ -390,13 +394,23 @@ namespace Avalonia.Controls if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { - IsPressed = true; - e.Handled = true; - - if (ClickMode == ClickMode.Press) + if (_isFlyoutOpen && IsEffectivelyEnabled) { + // When a flyout is open with OverlayDismissEventPassThrough enabled and the button is pressed, + // close the flyout, but do not transition to a pressed state + e.Handled = true; OnClick(); } + else + { + IsPressed = true; + e.Handled = true; + + if (ClickMode == ClickMode.Press) + { + OnClick(); + } + } } } @@ -451,25 +465,24 @@ namespace Avalonia.Controls if (change.Property == CommandProperty) { + var (oldValue, newValue) = change.GetOldAndNewValue(); if (((ILogical)this).IsAttachedToLogicalTree) { - var (oldValue, newValue) = change.GetOldAndNewValue(); if (oldValue is ICommand oldCommand) { - oldCommand.CanExecuteChanged -= CanExecuteChanged; + oldCommand.CanExecuteChanged -= CanExecuteChangedHandler; } if (newValue is ICommand newCommand) { - newCommand.CanExecuteChanged += CanExecuteChanged; + newCommand.CanExecuteChanged += CanExecuteChangedHandler; } } - - CanExecuteChanged(this, EventArgs.Empty); + CanExecuteChanged(newValue, CommandParameter); } else if (change.Property == CommandParameterProperty) { - CanExecuteChanged(this, EventArgs.Empty); + CanExecuteChanged(Command, change.NewValue); } else if (change.Property == IsCancelProperty) { @@ -557,7 +570,18 @@ namespace Avalonia.Controls /// The event args. private void CanExecuteChanged(object? sender, EventArgs e) { - var canExecute = Command == null || Command.CanExecute(CommandParameter); + CanExecuteChanged(Command, CommandParameter); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private void CanExecuteChanged(ICommand? command, object? parameter) + { + if (!((ILogical)this).IsAttachedToLogicalTree) + { + return; + } + + var canExecute = command == null || command.CanExecute(parameter); if (canExecute != _commandCanExecute) { diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index b570b2c4ff..ec54533f4e 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -629,10 +629,22 @@ namespace Avalonia.Controls private void DropDownButton_PointerPressed(object? sender, PointerPressedEventArgs e) { - _ignoreButtonClick = _isPopupClosing; + if (_isFlyoutOpen && (_dropDownButton?.IsEffectivelyEnabled == true) && e.GetCurrentPoint(_dropDownButton).Properties.IsLeftButtonPressed) + { + // When a flyout is open with OverlayDismissEventPassThrough enabled and the drop-down button + // is pressed, close the flyout + _ignoreButtonClick = true; - _isPressed = true; - UpdatePseudoClasses(); + e.Handled = true; + TogglePopUp(); + } + else + { + _ignoreButtonClick = _isPopupClosing; + + _isPressed = true; + UpdatePseudoClasses(); + } } private void DropDownButton_PointerReleased(object? sender, PointerReleasedEventArgs e) diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index 498f9cdf7f..d796fabb05 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -1,4 +1,6 @@ using System; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -94,5 +96,8 @@ namespace Avalonia.Controls.Chrome _captionButtons?.Detach(); _captionButtons = null; } + + /// + protected override AutomationPeer OnCreateAutomationPeer() => new TitleBarAutomationPeer(this); } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index f1cc84f7a4..dbdbf4b536 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -298,7 +298,18 @@ namespace Avalonia.Controls return; } } - PseudoClasses.Set(pcPressed, true); + + if (IsDropDownOpen) + { + // When a drop-down is open with OverlayDismissEventPassThrough enabled and the control + // is pressed, close the drop-down + SetCurrentValue(IsDropDownOpenProperty, false); + e.Handled = true; + } + else + { + PseudoClasses.Set(pcPressed, true); + } } /// @@ -314,7 +325,7 @@ namespace Avalonia.Controls e.Handled = true; } } - else + else if (PseudoClasses.Contains(pcPressed)) { SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen); e.Handled = true; @@ -375,11 +386,6 @@ namespace Avalonia.Controls { _subscriptionsOnOpen.Clear(); - if (CanFocus(this)) - { - Focus(); - } - DropDownClosed?.Invoke(this, EventArgs.Empty); } diff --git a/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs b/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs index 0fa43809ac..03b3706588 100644 --- a/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs +++ b/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.Converters } else if (value is KeyGesture gesture && targetType == typeof(string)) { - return ToPlatformString(gesture); + return gesture.ToString("p", null); } else { @@ -41,156 +41,7 @@ namespace Avalonia.Controls.Converters /// /// The gesture. /// The gesture formatted according to the current platform. - public static string ToPlatformString(KeyGesture gesture) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return ToString(gesture, "Win"); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return ToString(gesture, "Super"); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return ToOSXString(gesture); - } - else - { - return gesture.ToString(); - } - } - - private static string ToString(KeyGesture gesture, string meta) - { - var s = StringBuilderCache.Acquire(); - - static void Plus(StringBuilder s) - { - if (s.Length > 0) - { - s.Append("+"); - } - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Control)) - { - s.Append("Ctrl"); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Shift)) - { - Plus(s); - s.Append("Shift"); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Alt)) - { - Plus(s); - s.Append("Alt"); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) - { - Plus(s); - s.Append(meta); - } - - Plus(s); - s.Append(ToString(gesture.Key)); - - return StringBuilderCache.GetStringAndRelease(s); - } - - private static string ToOSXString(KeyGesture gesture) - { - var s = StringBuilderCache.Acquire(); - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Control)) - { - s.Append('⌃'); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Alt)) - { - s.Append('⌥'); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Shift)) - { - s.Append('⇧'); - } - - if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) - { - s.Append('⌘'); - } - - s.Append(ToOSXString(gesture.Key)); - - return StringBuilderCache.GetStringAndRelease(s); - } - - private static string ToString(Key key) - { - return key switch - { - Key.Add => "+", - Key.Back => "Backspace", - Key.D0 => "0", - Key.D1 => "1", - Key.D2 => "2", - Key.D3 => "3", - Key.D4 => "4", - Key.D5 => "5", - Key.D6 => "6", - Key.D7 => "7", - Key.D8 => "8", - Key.D9 => "9", - Key.Decimal => ".", - Key.Divide => "/", - Key.Down => "Down Arrow", - Key.Left => "Left Arrow", - Key.Multiply => "*", - Key.OemBackslash => "\\", - Key.OemCloseBrackets => "]", - Key.OemComma => ",", - Key.OemMinus => "-", - Key.OemOpenBrackets => "[", - Key.OemPeriod=> ".", - Key.OemPipe => "|", - Key.OemPlus => "+", - Key.OemQuestion => "/", - Key.OemQuotes => "\"", - Key.OemSemicolon => ";", - Key.OemTilde => "`", - Key.Right => "Right Arrow", - Key.Separator => "/", - Key.Subtract => "-", - Key.Up => "Up Arrow", - _ => key.ToString(), - }; - } - - private static string ToOSXString(Key key) - { - return key switch - { - Key.Back => "⌫", - Key.Down => "↓", - Key.End => "↘", - Key.Escape => "⎋", - Key.Home => "↖", - Key.Left => "←", - Key.Return => "↩", - Key.PageDown => "⇞", - Key.PageUp => "⇟", - Key.Right => "→", - Key.Space => "␣", - Key.Tab => "⇥", - Key.Up => "↑", - _ => ToString(key), - }; - } + public static string ToPlatformString(KeyGesture gesture) => gesture.ToString("p", null); + } } diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs index cbe2eddf53..e9cdd3039a 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -18,6 +18,7 @@ namespace Avalonia.Controls.Primitives Day, Hour, Minute, + Second, TimePeriod //AM or PM } @@ -516,6 +517,8 @@ namespace Avalonia.Controls.Primitives return new TimeSpan(value, 0, 0).ToString(ItemFormat); case DateTimePickerPanelType.Minute: return new TimeSpan(0, value, 0).ToString(ItemFormat); + case DateTimePickerPanelType.Second: + return new TimeSpan(0, 0, value).ToString(ItemFormat); case DateTimePickerPanelType.TimePeriod: return value == MinimumValue ? TimeUtils.GetAMDesignator() : TimeUtils.GetPMDesignator(); default: diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index 62ac76e71c..111c9ff623 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -18,12 +18,15 @@ namespace Avalonia.Controls [TemplatePart("PART_FlyoutButtonContentGrid", typeof(Grid))] [TemplatePart("PART_HourTextBlock", typeof(TextBlock))] [TemplatePart("PART_MinuteTextBlock", typeof(TextBlock))] + [TemplatePart("PART_SecondTextBlock", typeof(TextBlock))] [TemplatePart("PART_PeriodTextBlock", typeof(TextBlock))] [TemplatePart("PART_PickerPresenter", typeof(TimePickerPresenter))] [TemplatePart("PART_Popup", typeof(Popup))] [TemplatePart("PART_SecondColumnDivider", typeof(Rectangle))] [TemplatePart("PART_SecondPickerHost", typeof(Border))] - [TemplatePart("PART_ThirdPickerHost", typeof(Border))] + [TemplatePart("PART_ThirdColumnDivider", typeof(Rectangle))] + [TemplatePart("PART_ThirdPickerHost", typeof(Border))] + [TemplatePart("PART_FourthPickerHost", typeof(Border))] [PseudoClasses(":hasnotime")] public class TimePicker : TemplatedControl { @@ -32,12 +35,24 @@ namespace Avalonia.Controls /// public static readonly StyledProperty MinuteIncrementProperty = AvaloniaProperty.Register(nameof(MinuteIncrement), 1, coerce: CoerceMinuteIncrement); + + /// + /// Defines the property + /// + public static readonly StyledProperty SecondIncrementProperty = + AvaloniaProperty.Register(nameof(SecondIncrement), 1, coerce: CoerceSecondIncrement); /// /// Defines the property /// public static readonly StyledProperty ClockIdentifierProperty = AvaloniaProperty.Register(nameof(ClockIdentifier), "12HourClock", coerce: CoerceClockIdentifier); + + /// + /// Defines the property + /// + public static readonly StyledProperty UseSecondsProperty = + AvaloniaProperty.Register(nameof(UseSeconds), false, coerce: CoerceUseSeconds); /// /// Defines the property @@ -52,11 +67,14 @@ namespace Avalonia.Controls private Border? _firstPickerHost; private Border? _secondPickerHost; private Border? _thirdPickerHost; + private Border? _fourthPickerHost; private TextBlock? _hourText; private TextBlock? _minuteText; + private TextBlock? _secondText; private TextBlock? _periodText; private Rectangle? _firstSplitter; private Rectangle? _secondSplitter; + private Rectangle? _thirdSplitter; private Grid? _contentGrid; private Popup? _popup; @@ -85,6 +103,23 @@ namespace Avalonia.Controls return value; } + + /// + /// Gets or sets the second increment in the picker + /// + public int SecondIncrement + { + get => GetValue(SecondIncrementProperty); + set => SetValue(SecondIncrementProperty, value); + } + + private static int CoerceSecondIncrement(AvaloniaObject sender, int value) + { + if (value < 1 || value > 59) + throw new ArgumentOutOfRangeException(null, "1 >= SecondIncrement <= 59"); + + return value; + } /// /// Gets or sets the clock identifier, either 12HourClock or 24HourClock @@ -103,6 +138,24 @@ namespace Avalonia.Controls return value; } + + /// + /// Gets or sets the use seconds switch, either true or false + /// + public bool UseSeconds + { + + get => GetValue(UseSecondsProperty); + set => SetValue(UseSecondsProperty, value); + } + + private static bool CoerceUseSeconds(AvaloniaObject sender, bool value) + { + if (!(value == true || value == false)) + throw new ArgumentException("Invalid UseSeconds", default(bool).ToString()); + + return value; + } /// /// Gets or sets the selected time. Can be null. @@ -135,13 +188,16 @@ namespace Avalonia.Controls _firstPickerHost = e.NameScope.Find("PART_FirstPickerHost"); _secondPickerHost = e.NameScope.Find("PART_SecondPickerHost"); _thirdPickerHost = e.NameScope.Find("PART_ThirdPickerHost"); + _fourthPickerHost = e.NameScope.Find("PART_FourthPickerHost"); _hourText = e.NameScope.Find("PART_HourTextBlock"); _minuteText = e.NameScope.Find("PART_MinuteTextBlock"); + _secondText = e.NameScope.Find("PART_SecondTextBlock"); _periodText = e.NameScope.Find("PART_PeriodTextBlock"); _firstSplitter = e.NameScope.Find("PART_FirstColumnDivider"); _secondSplitter = e.NameScope.Find("PART_SecondColumnDivider"); + _thirdSplitter = e.NameScope.Find("PART_ThirdColumnDivider"); _contentGrid = e.NameScope.Find("PART_FlyoutButtonContentGrid"); @@ -160,7 +216,9 @@ namespace Avalonia.Controls _presenter.Dismissed += OnDismissPicker; _presenter[!TimePickerPresenter.MinuteIncrementProperty] = this[!MinuteIncrementProperty]; + _presenter[!TimePickerPresenter.SecondIncrementProperty] = this[!SecondIncrementProperty]; _presenter[!TimePickerPresenter.ClockIdentifierProperty] = this[!ClockIdentifierProperty]; + _presenter[!TimePickerPresenter.UseSecondsProperty] = this[!UseSecondsProperty]; } } @@ -172,11 +230,20 @@ namespace Avalonia.Controls { SetSelectedTimeText(); } + else if (change.Property == SecondIncrementProperty) + { + SetSelectedTimeText(); + } else if (change.Property == ClockIdentifierProperty) { SetGrid(); SetSelectedTimeText(); } + else if (change.Property == UseSecondsProperty) + { + SetGrid(); + SetSelectedTimeText(); + } else if (change.Property == SelectedTimeProperty) { var (oldValue, newValue) = change.GetOldAndNewValue(); @@ -196,29 +263,40 @@ namespace Avalonia.Controls columnsD.Add(new ColumnDefinition(GridLength.Star)); columnsD.Add(new ColumnDefinition(GridLength.Auto)); columnsD.Add(new ColumnDefinition(GridLength.Star)); + if (UseSeconds) + { + columnsD.Add(new ColumnDefinition(GridLength.Auto)); + columnsD.Add(new ColumnDefinition(GridLength.Star)); + } if (!use24HourClock) { columnsD.Add(new ColumnDefinition(GridLength.Auto)); columnsD.Add(new ColumnDefinition(GridLength.Star)); } - + _contentGrid.ColumnDefinitions = columnsD; + + _thirdPickerHost!.IsVisible = UseSeconds; + _secondSplitter!.IsVisible = UseSeconds; - _thirdPickerHost!.IsVisible = !use24HourClock; - _secondSplitter!.IsVisible = !use24HourClock; + _fourthPickerHost!.IsVisible = !use24HourClock; + _thirdSplitter!.IsVisible = !use24HourClock; + + var amPmColumn = (UseSeconds) ? 6 : 4; Grid.SetColumn(_firstPickerHost!, 0); Grid.SetColumn(_secondPickerHost!, 2); - - Grid.SetColumn(_thirdPickerHost, use24HourClock ? 0 : 4); + Grid.SetColumn(_thirdPickerHost!, UseSeconds ? 4 : 0); + Grid.SetColumn(_fourthPickerHost, use24HourClock ? 0 : amPmColumn); Grid.SetColumn(_firstSplitter!, 1); - Grid.SetColumn(_secondSplitter, use24HourClock ? 0 : 3); + Grid.SetColumn(_secondSplitter!, UseSeconds ? 3 : 0); + Grid.SetColumn(_thirdSplitter, use24HourClock ? 0 : amPmColumn-1); } private void SetSelectedTimeText() { - if (_hourText == null || _minuteText == null || _periodText == null) + if (_hourText == null || _minuteText == null || _secondText == null || _periodText == null) return; var time = SelectedTime; @@ -230,11 +308,12 @@ namespace Avalonia.Controls { var hr = newTime.Hours; hr = hr > 12 ? hr - 12 : hr == 0 ? 12 : hr; - newTime = new TimeSpan(hr, newTime.Minutes, 0); + newTime = new TimeSpan(hr, newTime.Minutes, newTime.Seconds); } _hourText.Text = newTime.ToString("%h"); _minuteText.Text = newTime.ToString("mm"); + _secondText.Text = newTime.ToString("ss"); PseudoClasses.Set(":hasnotime", false); _periodText.Text = time.Value.Hours >= 12 ? TimeUtils.GetPMDesignator() : TimeUtils.GetAMDesignator(); @@ -244,6 +323,7 @@ namespace Avalonia.Controls // By clearing local value, we reset text property to the value from the template. _hourText.ClearValue(TextBlock.TextProperty); _minuteText.ClearValue(TextBlock.TextProperty); + _secondText.ClearValue(TextBlock.TextProperty); PseudoClasses.Set(":hasnotime", true); _periodText.Text = DateTime.Now.Hour >= 12 ? TimeUtils.GetPMDesignator() : TimeUtils.GetAMDesignator(); diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs index 7f19d0545e..be5c2db6fa 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs @@ -19,12 +19,16 @@ namespace Avalonia.Controls [TemplatePart("PART_MinuteDownButton", typeof(RepeatButton))] [TemplatePart("PART_MinuteSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart("PART_MinuteUpButton", typeof(RepeatButton))] + [TemplatePart("PART_SecondDownButton", typeof(RepeatButton))] + [TemplatePart("PART_SecondHost", typeof(Panel), IsRequired = true)] + [TemplatePart("PART_SecondSelector", typeof(DateTimePickerPanel), IsRequired = true)] + [TemplatePart("PART_SecondUpButton", typeof(RepeatButton))] [TemplatePart("PART_PeriodDownButton", typeof(RepeatButton))] [TemplatePart("PART_PeriodHost", typeof(Panel), IsRequired = true)] [TemplatePart("PART_PeriodSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart("PART_PeriodUpButton", typeof(RepeatButton))] [TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)] - [TemplatePart("PART_SecondSpacer", typeof(Rectangle), IsRequired = true)] + [TemplatePart("PART_ThirdSpacer", typeof(Rectangle), IsRequired = true)] public class TimePickerPresenter : PickerPresenterBase { /// @@ -32,12 +36,24 @@ namespace Avalonia.Controls /// public static readonly StyledProperty MinuteIncrementProperty = TimePicker.MinuteIncrementProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty SecondIncrementProperty = + TimePicker.SecondIncrementProperty.AddOwner(); /// /// Defines the property /// public static readonly StyledProperty ClockIdentifierProperty = TimePicker.ClockIdentifierProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty UseSecondsProperty = + TimePicker.UseSecondsProperty.AddOwner(); /// /// Defines the property @@ -60,15 +76,20 @@ namespace Avalonia.Controls private Button? _acceptButton; private Button? _dismissButton; private Rectangle? _spacer2; + private Rectangle? _spacer3; + private Panel? _secondHost; private Panel? _periodHost; private DateTimePickerPanel? _hourSelector; private DateTimePickerPanel? _minuteSelector; + private DateTimePickerPanel? _secondSelector; private DateTimePickerPanel? _periodSelector; private Button? _hourUpButton; private Button? _minuteUpButton; + private Button? _secondUpButton; private Button? _periodUpButton; private Button? _hourDownButton; private Button? _minuteDownButton; + private Button? _secondDownButton; private Button? _periodDownButton; /// @@ -79,6 +100,15 @@ namespace Avalonia.Controls get => GetValue(MinuteIncrementProperty); set => SetValue(MinuteIncrementProperty, value); } + + /// + /// Gets or sets the second increment in the selector + /// + public int SecondIncrement + { + get => GetValue(SecondIncrementProperty); + set => SetValue(SecondIncrementProperty, value); + } /// /// Gets or sets the current clock identifier, either 12HourClock or 24HourClock @@ -88,6 +118,15 @@ namespace Avalonia.Controls get => GetValue(ClockIdentifierProperty); set => SetValue(ClockIdentifierProperty, value); } + + /// + /// Gets or sets the current clock identifier, either 12HourClock or 24HourClock + /// + public bool UseSeconds + { + get => GetValue(UseSecondsProperty); + set => SetValue(UseSecondsProperty, value); + } /// /// Gets or sets the current time @@ -104,12 +143,15 @@ namespace Avalonia.Controls _pickerContainer = e.NameScope.Get("PART_PickerContainer"); _periodHost = e.NameScope.Get("PART_PeriodHost"); + _secondHost = e.NameScope.Get("PART_SecondHost"); _hourSelector = e.NameScope.Get("PART_HourSelector"); _minuteSelector = e.NameScope.Get("PART_MinuteSelector"); + _secondSelector = e.NameScope.Get("PART_SecondSelector"); _periodSelector = e.NameScope.Get("PART_PeriodSelector"); - + _spacer2 = e.NameScope.Get("PART_SecondSpacer"); + _spacer3 = e.NameScope.Get("PART_ThirdSpacer"); _acceptButton = e.NameScope.Get