diff --git a/Avalonia.sln b/Avalonia.sln index a792774d94..1e2a3c6027 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -5,48 +5,26 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Visuals", "src\Avalonia.Visuals\Avalonia.Visuals.csproj", "{EB582467-6ABB-43A1-B052-E981BA910E3A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Layout", "src\Avalonia.Layout\Avalonia.Layout.csproj", "{42472427-4774-4C81-8AFF-9F27B8E31721}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Windows", "Windows", "{B39A8919-9F95-48FE-AD7B-76E08B509888}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32", "src\Windows\Avalonia.Win32\Avalonia.Win32.csproj", "{811A76CF-1CF6-440F-963B-BBE31BD72A82}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1", "src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj", "{3E908F67-5543-4879-A1DC-08EACE79B3CD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Input", "src\Avalonia.Input\Avalonia.Input.csproj", "{62024B2D-53EB-4638-B26B-85EEAA54866E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Interactivity", "src\Avalonia.Interactivity\Avalonia.Interactivity.csproj", "{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls", "src\Avalonia.Controls\Avalonia.Controls.csproj", "{D2221C82-4A25-4583-9B43-D791E3F6820C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Styling", "src\Avalonia.Styling\Avalonia.Styling.csproj", "{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Default", "src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Diagnostics", "src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj", "{7062AE20-5DCC-4442-9645-8195BDECE63E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Animation", "src\Avalonia.Animation\Avalonia.Animation.csproj", "{D211E587-D8BC-45B9-95A4-F297C8FA5200}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Styling.UnitTests", "tests\Avalonia.Styling.UnitTests\Avalonia.Styling.UnitTests.csproj", "{47ECDF59-DEF8-4C53-87B1-2098A3429059}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.UnitTests", "tests\Avalonia.Controls.UnitTests\Avalonia.Controls.UnitTests.csproj", "{5CCB5571-7C30-4E7D-967D-0E2158EBD91F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Visuals.UnitTests", "tests\Avalonia.Visuals.UnitTests\Avalonia.Visuals.UnitTests.csproj", "{76716382-3159-460E-BDA6-C5715CF606D7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base.UnitTests", "tests\Avalonia.Base.UnitTests\Avalonia.Base.UnitTests.csproj", "{2905FF23-53FB-45E6-AA49-6AF47A172056}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Layout.UnitTests", "tests\Avalonia.Layout.UnitTests\Avalonia.Layout.UnitTests.csproj", "{DB070A10-BF39-4752-8456-86E9D5928478}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Interactivity.UnitTests", "tests\Avalonia.Interactivity.UnitTests\Avalonia.Interactivity.UnitTests.csproj", "{08478EF5-44E8-42E9-92D6-15E00EC038D8}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1.RenderTests", "tests\Avalonia.Direct2D1.RenderTests\Avalonia.Direct2D1.RenderTests.csproj", "{DABFD304-D6A4-4752-8123-C2CCF7AC7831}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Input.UnitTests", "tests\Avalonia.Input.UnitTests\Avalonia.Input.UnitTests.csproj", "{AC18926A-E784-40FE-B09D-BB0FE2B599F0}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Direct2D1.UnitTests", "tests\Avalonia.Direct2D1.UnitTests\Avalonia.Direct2D1.UnitTests.csproj", "{EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.UnitTests", "tests\Avalonia.Markup.Xaml.UnitTests\Avalonia.Markup.Xaml.UnitTests.csproj", "{99135EAB-653D-47E4-A378-C96E1278CA44}" @@ -61,6 +39,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs + src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}" @@ -77,8 +56,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Android", "src\Android\Avalonia.Android\Avalonia.Android.csproj", "{7B92AF71-6287-4693-9DCB-BD5B6E927E23}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.AndroidTestApplication", "src\Android\Avalonia.AndroidTestApplication\Avalonia.AndroidTestApplication.csproj", "{FF69B927-C545-49AE-8E16-3D14D621AA12}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "iOS", "iOS", "{0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.iOS", "src\iOS\Avalonia.iOS\Avalonia.iOS.csproj", "{4488AD85-1495-4809-9AA4-DDFE0A48527E}" @@ -117,6 +94,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Base.props = build\Base.props build\Binding.props = build\Binding.props build\CoreLibraries.props = build\CoreLibraries.props + build\DevAnalyzers.props = build\DevAnalyzers.props build\EmbedXaml.props = build\EmbedXaml.props build\HarfBuzzSharp.props = build\HarfBuzzSharp.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props @@ -134,6 +112,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\SharedVersion.props = build\SharedVersion.props build\SharpDX.props = build\SharpDX.props build\SkiaSharp.props = build\SkiaSharp.props + build\SourceGenerators.props = build\SourceGenerators.props build\SourceLink.props = build\SourceLink.props build\System.Drawing.Common.props = build\System.Drawing.Common.props build\System.Memory.props = build\System.Memory.props @@ -184,8 +163,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Build.Tasks", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "nukebuild\_build.csproj", "{3F00BC43-5095-477F-93D8-E65B08179A00}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Animation.UnitTests", "tests\Avalonia.Animation.UnitTests\Avalonia.Animation.UnitTests.csproj", "{AF227847-E65C-4BE9-BCE9-B551357788E0}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}" @@ -232,9 +209,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -276,54 +257,6 @@ Global {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|iPhone.Build.0 = Release|Any CPU {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|Any CPU.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhone.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhone.Build.0 = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|Any CPU.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhone.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhone.Build.0 = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|Any CPU.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhone.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|Any CPU.Build.0 = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhone.Build.0 = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|Any CPU.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|Any CPU.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhone.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhone.Build.0 = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {42472427-4774-4C81-8AFF-9F27B8E31721}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -372,54 +305,6 @@ Global {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhone.Build.0 = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|Any CPU.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhone.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhone.Build.0 = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|Any CPU.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhone.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhone.Build.0 = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|Any CPU.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhone.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhone.Build.0 = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|Any CPU.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhone.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhone.Build.0 = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -444,30 +329,6 @@ Global {D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|iPhone.Build.0 = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|Any CPU.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhone.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhone.Build.0 = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|Any CPU.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhone.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhone.Build.0 = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -516,54 +377,6 @@ Global {7062AE20-5DCC-4442-9645-8195BDECE63E}.Release|iPhone.Build.0 = Release|Any CPU {7062AE20-5DCC-4442-9645-8195BDECE63E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {7062AE20-5DCC-4442-9645-8195BDECE63E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|Any CPU.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhone.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhone.Build.0 = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|Any CPU.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhone.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhone.Build.0 = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|Any CPU.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhone.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhone.Build.0 = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|Any CPU.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhone.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhone.Build.0 = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -588,30 +401,6 @@ Global {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|iPhone.Build.0 = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|Any CPU.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhone.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhone.Build.0 = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|Any CPU.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhone.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhone.Build.0 = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {76716382-3159-460E-BDA6-C5715CF606D7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -636,54 +425,6 @@ Global {2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|iPhone.Build.0 = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|Any CPU.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhone.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhone.Build.0 = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|Any CPU.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhone.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhone.Build.0 = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {DB070A10-BF39-4752-8456-86E9D5928478}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|Any CPU.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhone.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhone.Build.0 = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|Any CPU.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhone.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhone.Build.0 = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {08478EF5-44E8-42E9-92D6-15E00EC038D8}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -708,30 +449,6 @@ Global {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|iPhone.Build.0 = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|Any CPU.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhone.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhone.Build.0 = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|Any CPU.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhone.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhone.Build.0 = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -914,22 +631,6 @@ Global {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|Any CPU.Build.0 = Release|Any CPU {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|iPhone.ActiveCfg = Release|Any CPU {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Build.0 = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Deploy.0 = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -1638,30 +1339,6 @@ Global {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|iPhone.Build.0 = Release|Any CPU {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhone.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhone.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|Any CPU.Build.0 = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhone.ActiveCfg = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhone.Build.0 = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU @@ -2238,6 +1915,54 @@ Global {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.Build.0 = Release|Any CPU {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.Build.0 = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.ActiveCfg = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.Build.0 = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2245,14 +1970,9 @@ Global GlobalSection(NestedProjects) = preSolution {811A76CF-1CF6-440F-963B-BBE31BD72A82} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {3E908F67-5543-4879-A1DC-08EACE79B3CD} = {B39A8919-9F95-48FE-AD7B-76E08B509888} - {47ECDF59-DEF8-4C53-87B1-2098A3429059} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {5CCB5571-7C30-4E7D-967D-0E2158EBD91F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {76716382-3159-460E-BDA6-C5715CF606D7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {2905FF23-53FB-45E6-AA49-6AF47A172056} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {DB070A10-BF39-4752-8456-86E9D5928478} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {08478EF5-44E8-42E9-92D6-15E00EC038D8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {DABFD304-D6A4-4752-8123-C2CCF7AC7831} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {AC18926A-E784-40FE-B09D-BB0FE2B599F0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {99135EAB-653D-47E4-A378-C96E1278CA44} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3E53A01A-B331-47F3-B828-4A5717E77A24} = {8B6A8209-894F-4BA1-B880-965FD453982C} @@ -2260,7 +1980,6 @@ Global {8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098} {7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} - {FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} {4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1} {E1AA3DBF-9056-4530-9376-18119A7A3FFE} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {88060192-33D5-4932-B0F9-8BD2763E857D} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} @@ -2286,7 +2005,6 @@ Global {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {D49233F8-F29C-47DD-9975-C4C9E4502720} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} {3C471044-3640-45E3-B1B2-16D2FF8399EE} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C} - {AF227847-E65C-4BE9-BCE9-B551357788E0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} @@ -2303,6 +2021,7 @@ Global {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098} {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {CE910927-CE5A-456F-BC92-E4C757354A5C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/Directory.Build.props b/Directory.Build.props index 835decc672..97781b7517 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,5 +5,6 @@ false false + False diff --git a/NuGet.Config b/NuGet.Config index 7a1f28bea7..7d2bd8abd2 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -5,5 +5,6 @@ + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 79456b117b..edf3c3d819 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -31,14 +31,14 @@ jobs: vmImage: 'ubuntu-20.04' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.414' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.414 + version: 3.1.418 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.100' + displayName: 'Use .NET Core SDK 6.0.202' inputs: - version: 6.0.200 + version: 6.0.202 - task: CmdLine@2 displayName: 'Run Build' @@ -62,14 +62,14 @@ jobs: vmImage: 'macOS-10.15' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.414' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.414 + version: 3.1.418 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.100' + displayName: 'Use .NET Core SDK 6.0.202' inputs: - version: 6.0.200 + version: 6.0.202 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -134,20 +134,20 @@ jobs: SolutionDir: '$(Build.SourcesDirectory)' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.414' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.414 + version: 3.1.418 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.100' + displayName: 'Use .NET Core SDK 6.0.202' inputs: - version: 6.0.200 + version: 6.0.202 - task: CmdLine@2 displayName: 'Install Workloads' inputs: script: | - dotnet workload install --no-cache --disable-parallel android ios --skip-manifest-update --source "https://api.nuget.org/v3/index.json" + dotnet workload install android ios - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index 6bf69603c0..314d38190a 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -1,14 +1,8 @@ - - - - - - diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props new file mode 100644 index 0000000000..14e4f6a563 --- /dev/null +++ b/build/DevAnalyzers.props @@ -0,0 +1,9 @@ + + + + + diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 6dd6cccb53..e10de93530 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 60bebaad40..a217a8272d 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props new file mode 100644 index 0000000000..d000af1bf6 --- /dev/null +++ b/build/SourceGenerators.props @@ -0,0 +1,10 @@ + + + + + + diff --git a/global.json b/global.json index 30265268dc..a6792b05c7 100644 --- a/global.json +++ b/global.json @@ -1,11 +1,10 @@ { "sdk": { - "version": "6.0.200", + "version": "6.0.202", "rollForward": "latestFeature" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", - "Xamarin.Legacy.Sdk": "0.1.2-alpha6", "MSBuild.Sdk.Extras": "3.0.22", "AggregatePackage.NuGet.Sdk" : "0.1.12" } diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 79175d9ff1..05b129baca 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -73,6 +73,11 @@ ComPtr _events; _isHandlingSendEvent = true; @try { [super sendEvent: event]; + if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & NSEventModifierFlagCommand)) + { + [[self keyWindow] sendEvent:event]; + } + } @finally { _isHandlingSendEvent = oldHandling; } diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 620b750a40..4426e7fdff 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -457,7 +457,7 @@ public: } point = ConvertPointY(point); - NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; + 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)]; @@ -2461,6 +2461,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent if(_parent != nullptr) { + auto cparent = dynamic_cast(_parent.getRaw()); + + if(cparent != nullptr) + { + if(cparent->WindowState() == Maximized) + { + cparent->SetWindowState(Normal); + } + } + _parent->GetPosition(&position); _parent->BaseEvents->PositionChanged(position); } diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 72d90abbf3..f0f677b844 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -214,19 +214,14 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("Avalonia.Animation.UnitTests"); RunCoreTest("Avalonia.Base.UnitTests"); RunCoreTest("Avalonia.Controls.UnitTests"); RunCoreTest("Avalonia.Controls.DataGrid.UnitTests"); - RunCoreTest("Avalonia.Input.UnitTests"); - RunCoreTest("Avalonia.Interactivity.UnitTests"); - RunCoreTest("Avalonia.Layout.UnitTests"); RunCoreTest("Avalonia.Markup.UnitTests"); RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); - RunCoreTest("Avalonia.Styling.UnitTests"); - RunCoreTest("Avalonia.Visuals.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.PlatformSupport.UnitTests"); }); Target RunRenderTests => _ => _ diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 9777bb46c3..04c67e84e8 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -39,9 +39,9 @@ - + - + \ No newline at end of file diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index d1b657722c..2b45ac1508 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -6,7 +6,7 @@ true - + true https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json 7.0.0-* @@ -22,12 +22,11 @@ - + - diff --git a/samples/ControlCatalog.NetCore/rd.xml b/samples/ControlCatalog.NetCore/rd.xml deleted file mode 100644 index 27db7f34ca..0000000000 --- a/samples/ControlCatalog.NetCore/rd.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj index 520bbdf32b..b2c9ec72eb 100644 --- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj +++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj @@ -1,15 +1,14 @@  net6.0 - false enable - True + + true + 16777216 + false + false - - - - false @@ -23,19 +22,36 @@ -O3 -O3 false + false + false + false + false + false + true + false + true + true + true + link + true - - + + - - - - - + + + + + + + + + + diff --git a/samples/ControlCatalog.Web/LinkerConfig.xml b/samples/ControlCatalog.Web/LinkerConfig.xml deleted file mode 100644 index 5839a0fe03..0000000000 --- a/samples/ControlCatalog.Web/LinkerConfig.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index 12d1d5645e..513ac44f83 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -7,10 +7,10 @@ True iossimulator-x64 - + - + \ No newline at end of file diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index f3b52428ca..85f278b5fa 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -190,6 +190,14 @@ Mica + + + LeftToRight + RightToLeft + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 79cf07c8d9..e8ea39abbb 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -76,6 +76,15 @@ namespace ControlCatalog } }; + var flowDirections = this.Find("FlowDirection"); + flowDirections.SelectionChanged += (sender, e) => + { + if (flowDirections.SelectedItem is FlowDirection flowDirection) + { + this.FlowDirection = flowDirection; + } + }; + var decorations = this.Find("Decorations"); decorations.SelectionChanged += (sender, e) => { diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml b/samples/ControlCatalog/Pages/ButtonsPage.xaml index 7de0872eae..059b4d9788 100644 --- a/samples/ControlCatalog/Pages/ButtonsPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml @@ -147,6 +147,35 @@ + + + + + A button with an added drop-down chevron to visually indicate it has a flyout with additional actions. + + + + + + + + + + + + Disabled + + + + + Content="Re-themed" + Foreground="White"> + + + + @@ -31,7 +44,9 @@ - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 8305d72d1f..1d42b92096 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -53,10 +53,12 @@ UniformGrid - Horizontal + + ("scroller"); _scrollToLast = this.FindControl + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index b9e631a312..9a612aa94d 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -62,6 +62,8 @@ namespace IntegrationTestApp this.FindControl("BasicComboBox").SelectedIndex = 0; if (source?.Name == "ListBoxSelectionClear") this.FindControl("BasicListBox").SelectedIndex = -1; + if (source?.Name == "MenuClickedMenuItemReset") + this.FindControl("ClickedMenuItem").Text = "None"; } } } diff --git a/samples/RenderDemo/Pages/PathMeasurementPage.cs b/samples/RenderDemo/Pages/PathMeasurementPage.cs index 212377deae..cc5125609c 100644 --- a/samples/RenderDemo/Pages/PathMeasurementPage.cs +++ b/samples/RenderDemo/Pages/PathMeasurementPage.cs @@ -1,15 +1,9 @@ -using System; -using System.Diagnostics; -using System.Drawing.Drawing2D; -using System.Security.Cryptography; using Avalonia; using Avalonia.Controls; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Media.Immutable; -using Avalonia.Threading; -using Avalonia.Visuals.Media.Imaging; namespace RenderDemo.Pages { diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs index f263786ab7..f365b59c20 100644 --- a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs +++ b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs @@ -5,7 +5,6 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; -using Avalonia.Visuals.Media.Imaging; namespace RenderDemo.Pages { diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj index 3d5aee49e9..18a4ee5662 100644 --- a/samples/RenderDemo/RenderDemo.csproj +++ b/samples/RenderDemo/RenderDemo.csproj @@ -4,7 +4,7 @@ net6.0 - + TextFormatterPage.axaml Code diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs index bbfd3d87ca..ab61dcde91 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs +++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs @@ -43,14 +43,13 @@ namespace ControlSamples _splitView = e.NameScope.Find("PART_NavigationPane"); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == BoundsProperty && _splitView is not null) { - var oldBounds = change.OldValue.GetValueOrDefault(); - var newBounds = change.NewValue.GetValueOrDefault(); + var (oldBounds, newBounds) = change.GetOldAndNewValue(); EnsureSplitViewMode(oldBounds, newBounds); } } diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 203c3accd6..6688dde8f5 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,7 +1,6 @@ - + - net6.0-android - $(TargetFrameworks);monoandroid11.0 + net6.0-android 21 true true @@ -16,4 +15,5 @@ + diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj deleted file mode 100644 index db0bb01410..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ /dev/null @@ -1,43 +0,0 @@ - - - net6.0-android - 21 - Exe - enable - com.Avalonia.AndroidTestApplication - 1 - 1.0 - apk - true - portable - - - - - Resources\drawable\Icon.png - - - - - True - True - True - True - - - - - - - True - - - - True - - - - - - - diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs deleted file mode 100644 index 8f4beb2737..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using Android.App; -using Android.Content.PM; -using Avalonia.Android; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Input.TextInput; -using Avalonia.Markup.Xaml; -using Avalonia.Media; -using Avalonia.Styling; -using Avalonia.Themes.Default; - -namespace Avalonia.AndroidTestApplication -{ - [Activity(Label = "Main", - MainLauncher = true, - Icon = "@drawable/icon", - Theme = "@style/Theme.AppCompat.NoActionBar", - ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, - LaunchMode = LaunchMode.SingleInstance/*, - ScreenOrientation = ScreenOrientation.Landscape*/)] - public class MainActivity : AvaloniaActivity - { - protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) - { - return base.CustomizeAppBuilder(builder); - } - } - - public class App : Application - { - public override void Initialize() - { - Styles.Add(new SimpleTheme(new Uri("avares://Avalonia.AndroidTestApplication"))); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) - { - singleViewLifetime.MainView = CreateSimpleWindow(); - } - - base.OnFrameworkInitializationCompleted(); - } - - // This provides a simple UI tree for testing input handling, drawing, etc - public static ContentControl CreateSimpleWindow() - { - ContentControl window = new ContentControl() - { - Background = Brushes.Red, - Content = new StackPanel - { - Margin = new Thickness(30), - Background = Brushes.Yellow, - Children = - { - new TextBlock - { - Text = "TEXT BLOCK", - Width = 300, - Height = 40, - Background = Brushes.White, - Foreground = Brushes.Black - }, - - new Button - { - Content = "BUTTON", - Width = 150, - Height = 40, - Background = Brushes.LightGreen, - Foreground = Brushes.Black - }, - - CreateTextBox(TextInputContentType.Normal), - CreateTextBox(TextInputContentType.Password), - CreateTextBox(TextInputContentType.Email), - CreateTextBox(TextInputContentType.Url), - CreateTextBox(TextInputContentType.Digits), - CreateTextBox(TextInputContentType.Number), - } - } - }; - - return window; - } - - private static TextBox CreateTextBox(TextInputContentType contentType) - { - var textBox = new TextBox() - { - Margin = new Thickness(20, 10), - Watermark = contentType.ToString(), - BorderThickness = new Thickness(3), - FontSize = 20, - [TextInputOptions.ContentTypeProperty] = contentType - }; - - return textBox; - } - } -} diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml deleted file mode 100644 index ad8134f628..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt b/src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt deleted file mode 100644 index 194ae28a59..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt +++ /dev/null @@ -1,50 +0,0 @@ -Images, layout descriptions, binary blobs and string dictionaries can be included -in your application as resource files. Various Android APIs are designed to -operate on the resource IDs instead of dealing with images, strings or binary blobs -directly. - -For example, a sample Android app that contains a user interface layout (main.xml), -an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) -would keep its resources in the "Resources" directory of the application: - -Resources/ - drawable-hdpi/ - icon.png - - drawable-ldpi/ - icon.png - - drawable-mdpi/ - icon.png - - layout/ - main.xml - - values/ - strings.xml - -In order to get the build system to recognize Android resources, set the build action to -"AndroidResource". The native Android APIs do not operate directly with filenames, but -instead operate on resource IDs. When you compile an Android application that uses resources, -the build system will package the resources for distribution and generate a class called -"Resource" that contains the tokens for each one of the resources included. For example, -for the above Resources layout, this is what the Resource class would expose: - -public class Resource { - public class drawable { - public const int icon = 0x123; - } - - public class layout { - public const int main = 0x456; - } - - public class strings { - public const int first_string = 0xabc; - public const int second_string = 0xbcd; - } -} - -You would then use R.drawable.icon to reference the drawable/icon.png file, or Resource.layout.main -to reference the layout/main.xml file, or Resource.strings.first_string to reference the first -string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/drawable/Icon.png b/src/Android/Avalonia.AndroidTestApplication/Resources/drawable/Icon.png deleted file mode 100644 index 8074c4c571..0000000000 Binary files a/src/Android/Avalonia.AndroidTestApplication/Resources/drawable/Icon.png and /dev/null differ diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml b/src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml deleted file mode 100644 index c8dca13c35..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - Hello World, Click Me! - Avalonia.AndroidTestApplication - \ No newline at end of file diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs deleted file mode 100644 index 50fc5ac73b..0000000000 --- a/src/Avalonia.Animation/Animatable.cs +++ /dev/null @@ -1,253 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using Avalonia.Data; - -#nullable enable - -namespace Avalonia.Animation -{ - /// - /// Base class for all animatable objects. - /// - public class Animatable : AvaloniaObject - { - /// - /// Defines the property. - /// - public static readonly StyledProperty ClockProperty = - AvaloniaProperty.Register(nameof(Clock), inherits: true); - - /// - /// Defines the property. - /// - public static readonly StyledProperty TransitionsProperty = - AvaloniaProperty.Register(nameof(Transitions)); - - private bool _transitionsEnabled = true; - private Dictionary? _transitionState; - - /// - /// Gets or sets the clock which controls the animations on the control. - /// - public IClock Clock - { - get => GetValue(ClockProperty); - set => SetValue(ClockProperty, value); - } - - /// - /// Gets or sets the property transitions for the control. - /// - public Transitions? Transitions - { - get => GetValue(TransitionsProperty); - set => SetValue(TransitionsProperty, value); - } - - /// - /// Enables transitions for the control. - /// - /// - /// This method should not be called from user code, it will be called automatically by the framework - /// when a control is added to the visual tree. - /// - protected void EnableTransitions() - { - if (!_transitionsEnabled) - { - _transitionsEnabled = true; - - if (Transitions is object) - { - AddTransitions(Transitions); - } - } - } - - /// - /// Disables transitions for the control. - /// - /// - /// This method should not be called from user code, it will be called automatically by the framework - /// when a control is added to the visual tree. - /// - protected void DisableTransitions() - { - if (_transitionsEnabled) - { - _transitionsEnabled = false; - - if (Transitions is object) - { - RemoveTransitions(Transitions); - } - } - } - - protected sealed override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) - { - if (change.Property == TransitionsProperty && change.IsEffectiveValueChange) - { - var oldTransitions = change.OldValue.GetValueOrDefault(); - var newTransitions = change.NewValue.GetValueOrDefault(); - - // When transitions are replaced, we add the new transitions before removing the old - // transitions, so that when the old transition being disposed causes the value to - // change, there is a corresponding entry in `_transitionStates`. This means that we - // need to account for any transitions present in both the old and new transitions - // collections. - if (newTransitions is object) - { - var toAdd = (IList)newTransitions; - - if (newTransitions.Count > 0 && oldTransitions?.Count > 0) - { - toAdd = newTransitions.Except(oldTransitions).ToList(); - } - - newTransitions.CollectionChanged += TransitionsCollectionChanged; - AddTransitions(toAdd); - } - - if (oldTransitions is object) - { - var toRemove = (IList)oldTransitions; - - if (oldTransitions.Count > 0 && newTransitions?.Count > 0) - { - toRemove = oldTransitions.Except(newTransitions).ToList(); - } - - oldTransitions.CollectionChanged -= TransitionsCollectionChanged; - RemoveTransitions(toRemove); - } - } - else if (_transitionsEnabled && - Transitions is object && - _transitionState is object && - !change.Property.IsDirect && - change.Priority > BindingPriority.Animation) - { - for (var i = Transitions.Count -1; i >= 0; --i) - { - var transition = Transitions[i]; - - if (transition.Property == change.Property && - _transitionState.TryGetValue(transition, out var state)) - { - var oldValue = state.BaseValue; - var newValue = GetAnimationBaseValue(transition.Property); - - if (!Equals(oldValue, newValue)) - { - state.BaseValue = newValue; - - // We need to transition from the current animated value if present, - // instead of the old base value. - var animatedValue = GetValue(transition.Property); - - if (!Equals(newValue, animatedValue)) - { - oldValue = animatedValue; - } - - state.Instance?.Dispose(); - state.Instance = transition.Apply( - this, - Clock ?? AvaloniaLocator.Current.GetRequiredService(), - oldValue, - newValue); - return; - } - } - } - } - - base.OnPropertyChangedCore(change); - } - - private void TransitionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - if (!_transitionsEnabled) - { - return; - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - AddTransitions(e.NewItems!); - break; - case NotifyCollectionChangedAction.Remove: - RemoveTransitions(e.OldItems!); - break; - case NotifyCollectionChangedAction.Replace: - RemoveTransitions(e.OldItems!); - AddTransitions(e.NewItems!); - break; - case NotifyCollectionChangedAction.Reset: - throw new NotSupportedException("Transitions collection cannot be reset."); - } - } - - private void AddTransitions(IList items) - { - if (!_transitionsEnabled) - { - return; - } - - _transitionState ??= new Dictionary(); - - for (var i = 0; i < items.Count; ++i) - { - var t = (ITransition)items[i]!; - - _transitionState.Add(t, new TransitionState - { - BaseValue = GetAnimationBaseValue(t.Property), - }); - } - } - - private void RemoveTransitions(IList items) - { - if (_transitionState is null) - { - return; - } - - for (var i = 0; i < items.Count; ++i) - { - var t = (ITransition)items[i]!; - - if (_transitionState.TryGetValue(t, out var state)) - { - state.Instance?.Dispose(); - _transitionState.Remove(t); - } - } - } - - private object? GetAnimationBaseValue(AvaloniaProperty property) - { - var value = this.GetBaseValue(property, BindingPriority.LocalValue); - - if (value == AvaloniaProperty.UnsetValue) - { - value = GetValue(property); - } - - return value; - } - - private class TransitionState - { - public IDisposable? Instance { get; set; } - public object? BaseValue { get; set; } - } - } -} diff --git a/src/Avalonia.Animation/ApiCompatBaseline.txt b/src/Avalonia.Animation/ApiCompatBaseline.txt deleted file mode 100644 index 973698f872..0000000000 --- a/src/Avalonia.Animation/ApiCompatBaseline.txt +++ /dev/null @@ -1,5 +0,0 @@ -Compat issues with assembly Avalonia.Animation: -InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' is present in the contract but not in the implementation. -MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock, System.Threading.CancellationToken)' is present in the implementation but not in the contract. -Total Issues: 3 diff --git a/src/Avalonia.Animation/Avalonia.Animation.csproj b/src/Avalonia.Animation/Avalonia.Animation.csproj deleted file mode 100644 index d81d14bbff..0000000000 --- a/src/Avalonia.Animation/Avalonia.Animation.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - net6.0;netstandard2.0 - - - - - - - - - - - diff --git a/src/Avalonia.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs deleted file mode 100644 index c721772f3e..0000000000 --- a/src/Avalonia.Animation/Easing/Easing.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.Linq; - -namespace Avalonia.Animation.Easings -{ - /// - /// Base class for all Easing classes. - /// - [TypeConverter(typeof(EasingTypeConverter))] - public abstract class Easing : IEasing - { - /// - public abstract double Ease(double progress); - - static Dictionary? _easingTypes; - - static readonly Type s_thisType = typeof(Easing); - - /// - /// Parses a Easing type string. - /// - /// The Easing type string. - /// Returns the instance of the parsed type. - public static Easing Parse(string e) - { - if (e.Contains(',')) - { - return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture)); - } - - if (_easingTypes == null) - { - _easingTypes = new Dictionary(); - - // Fetch the built-in easings. - var derivedTypes = typeof(Easing).Assembly.GetTypes() - .Where(p => p.Namespace == s_thisType.Namespace) - .Where(p => p.IsSubclassOf(s_thisType)) - .Select(p => p); - - foreach (var easingType in derivedTypes) - _easingTypes.Add(easingType.Name, easingType); - } - - if (_easingTypes.ContainsKey(e)) - { - var type = _easingTypes[e]; - return (Easing)Activator.CreateInstance(type)!; - } - else - { - throw new FormatException($"Easing \"{e}\" was not found in {s_thisType.Namespace} namespace."); - } - } - } -} diff --git a/src/Avalonia.Animation/Properties/AssemblyInfo.cs b/src/Avalonia.Animation/Properties/AssemblyInfo.cs deleted file mode 100644 index 6b539b075b..0000000000 --- a/src/Avalonia.Animation/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Avalonia.Metadata; -using System.Runtime.CompilerServices; - -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")] - -[assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs new file mode 100644 index 0000000000..b045a32cd1 --- /dev/null +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Animation +{ + /// + /// Base class for all animatable objects. + /// + public class Animatable : AvaloniaObject + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ClockProperty = + AvaloniaProperty.Register(nameof(Clock), inherits: true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty TransitionsProperty = + AvaloniaProperty.Register(nameof(Transitions)); + + private bool _transitionsEnabled = true; + private Dictionary? _transitionState; + + /// + /// Gets or sets the clock which controls the animations on the control. + /// + public IClock Clock + { + get => GetValue(ClockProperty); + set => SetValue(ClockProperty, value); + } + + /// + /// Gets or sets the property transitions for the control. + /// + public Transitions? Transitions + { + get => GetValue(TransitionsProperty); + set => SetValue(TransitionsProperty, value); + } + + /// + /// Enables transitions for the control. + /// + /// + /// This method should not be called from user code, it will be called automatically by the framework + /// when a control is added to the visual tree. + /// + protected void EnableTransitions() + { + if (!_transitionsEnabled) + { + _transitionsEnabled = true; + + if (Transitions is object) + { + AddTransitions(Transitions); + } + } + } + + /// + /// Disables transitions for the control. + /// + /// + /// This method should not be called from user code, it will be called automatically by the framework + /// when a control is added to the visual tree. + /// + protected void DisableTransitions() + { + if (_transitionsEnabled) + { + _transitionsEnabled = false; + + if (Transitions is object) + { + RemoveTransitions(Transitions); + } + } + } + + protected sealed override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == TransitionsProperty && change.IsEffectiveValueChange) + { + var e = (AvaloniaPropertyChangedEventArgs)change; + var oldTransitions = e.OldValue.GetValueOrDefault(); + var newTransitions = e.NewValue.GetValueOrDefault(); + + // When transitions are replaced, we add the new transitions before removing the old + // transitions, so that when the old transition being disposed causes the value to + // change, there is a corresponding entry in `_transitionStates`. This means that we + // need to account for any transitions present in both the old and new transitions + // collections. + if (newTransitions is object) + { + var toAdd = (IList)newTransitions; + + if (newTransitions.Count > 0 && oldTransitions?.Count > 0) + { + toAdd = newTransitions.Except(oldTransitions).ToList(); + } + + newTransitions.CollectionChanged += TransitionsCollectionChanged; + AddTransitions(toAdd); + } + + if (oldTransitions is object) + { + var toRemove = (IList)oldTransitions; + + if (oldTransitions.Count > 0 && newTransitions?.Count > 0) + { + toRemove = oldTransitions.Except(newTransitions).ToList(); + } + + oldTransitions.CollectionChanged -= TransitionsCollectionChanged; + RemoveTransitions(toRemove); + } + } + else if (_transitionsEnabled && + Transitions is object && + _transitionState is object && + !change.Property.IsDirect && + change.Priority > BindingPriority.Animation) + { + for (var i = Transitions.Count -1; i >= 0; --i) + { + var transition = Transitions[i]; + + if (transition.Property == change.Property && + _transitionState.TryGetValue(transition, out var state)) + { + var oldValue = state.BaseValue; + var newValue = GetAnimationBaseValue(transition.Property); + + if (!Equals(oldValue, newValue)) + { + state.BaseValue = newValue; + + // We need to transition from the current animated value if present, + // instead of the old base value. + var animatedValue = GetValue(transition.Property); + + if (!Equals(newValue, animatedValue)) + { + oldValue = animatedValue; + } + + state.Instance?.Dispose(); + state.Instance = transition.Apply( + this, + Clock ?? AvaloniaLocator.Current.GetRequiredService(), + oldValue, + newValue); + return; + } + } + } + } + + base.OnPropertyChangedCore(change); + } + + private void TransitionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (!_transitionsEnabled) + { + return; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + AddTransitions(e.NewItems!); + break; + case NotifyCollectionChangedAction.Remove: + RemoveTransitions(e.OldItems!); + break; + case NotifyCollectionChangedAction.Replace: + RemoveTransitions(e.OldItems!); + AddTransitions(e.NewItems!); + break; + case NotifyCollectionChangedAction.Reset: + throw new NotSupportedException("Transitions collection cannot be reset."); + } + } + + private void AddTransitions(IList items) + { + if (!_transitionsEnabled) + { + return; + } + + _transitionState ??= new Dictionary(); + + for (var i = 0; i < items.Count; ++i) + { + var t = (ITransition)items[i]!; + + _transitionState.Add(t, new TransitionState + { + BaseValue = GetAnimationBaseValue(t.Property), + }); + } + } + + private void RemoveTransitions(IList items) + { + if (_transitionState is null) + { + return; + } + + for (var i = 0; i < items.Count; ++i) + { + var t = (ITransition)items[i]!; + + if (_transitionState.TryGetValue(t, out var state)) + { + state.Instance?.Dispose(); + _transitionState.Remove(t); + } + } + } + + private object? GetAnimationBaseValue(AvaloniaProperty property) + { + var value = this.GetBaseValue(property, BindingPriority.LocalValue); + + if (value == AvaloniaProperty.UnsetValue) + { + value = GetValue(property); + } + + return value; + } + + private class TransitionState + { + public IDisposable? Instance { get; set; } + public object? BaseValue { get; set; } + } + } +} diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs similarity index 100% rename from src/Avalonia.Animation/Animation.cs rename to src/Avalonia.Base/Animation/Animation.cs diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs similarity index 100% rename from src/Avalonia.Animation/AnimationInstance`1.cs rename to src/Avalonia.Base/Animation/AnimationInstance`1.cs diff --git a/src/Avalonia.Animation/AnimatorDrivenTransition.cs b/src/Avalonia.Base/Animation/AnimatorDrivenTransition.cs similarity index 100% rename from src/Avalonia.Animation/AnimatorDrivenTransition.cs rename to src/Avalonia.Base/Animation/AnimatorDrivenTransition.cs diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs similarity index 100% rename from src/Avalonia.Animation/AnimatorKeyFrame.cs rename to src/Avalonia.Base/Animation/AnimatorKeyFrame.cs diff --git a/src/Avalonia.Animation/AnimatorTransitionObservable.cs b/src/Avalonia.Base/Animation/AnimatorTransitionObservable.cs similarity index 100% rename from src/Avalonia.Animation/AnimatorTransitionObservable.cs rename to src/Avalonia.Base/Animation/AnimatorTransitionObservable.cs diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs similarity index 100% rename from src/Avalonia.Animation/Animators/Animator`1.cs rename to src/Avalonia.Base/Animation/Animators/Animator`1.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/BaseBrushAnimator.cs rename to src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs diff --git a/src/Avalonia.Animation/Animators/BoolAnimator.cs b/src/Avalonia.Base/Animation/Animators/BoolAnimator.cs similarity index 100% rename from src/Avalonia.Animation/Animators/BoolAnimator.cs rename to src/Avalonia.Base/Animation/Animators/BoolAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs b/src/Avalonia.Base/Animation/Animators/BoxShadowAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs rename to src/Avalonia.Base/Animation/Animators/BoxShadowAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs b/src/Avalonia.Base/Animation/Animators/BoxShadowsAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs rename to src/Avalonia.Base/Animation/Animators/BoxShadowsAnimator.cs diff --git a/src/Avalonia.Animation/Animators/ByteAnimator.cs b/src/Avalonia.Base/Animation/Animators/ByteAnimator.cs similarity index 100% rename from src/Avalonia.Animation/Animators/ByteAnimator.cs rename to src/Avalonia.Base/Animation/Animators/ByteAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/ColorAnimator.cs b/src/Avalonia.Base/Animation/Animators/ColorAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/ColorAnimator.cs rename to src/Avalonia.Base/Animation/Animators/ColorAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/CornerRadiusAnimator.cs b/src/Avalonia.Base/Animation/Animators/CornerRadiusAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/CornerRadiusAnimator.cs rename to src/Avalonia.Base/Animation/Animators/CornerRadiusAnimator.cs diff --git a/src/Avalonia.Animation/Animators/DecimalAnimator.cs b/src/Avalonia.Base/Animation/Animators/DecimalAnimator.cs similarity index 100% rename from src/Avalonia.Animation/Animators/DecimalAnimator.cs rename to src/Avalonia.Base/Animation/Animators/DecimalAnimator.cs diff --git a/src/Avalonia.Animation/Animators/DoubleAnimator.cs b/src/Avalonia.Base/Animation/Animators/DoubleAnimator.cs similarity index 100% rename from src/Avalonia.Animation/Animators/DoubleAnimator.cs rename to src/Avalonia.Base/Animation/Animators/DoubleAnimator.cs diff --git a/src/Avalonia.Animation/Animators/FloatAnimator.cs b/src/Avalonia.Base/Animation/Animators/FloatAnimator.cs similarity index 100% rename from src/Avalonia.Animation/Animators/FloatAnimator.cs rename to src/Avalonia.Base/Animation/Animators/FloatAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/GradientBrushAnimator.cs rename to src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs diff --git a/src/Avalonia.Animation/Animators/Int16Animator.cs b/src/Avalonia.Base/Animation/Animators/Int16Animator.cs similarity index 100% rename from src/Avalonia.Animation/Animators/Int16Animator.cs rename to src/Avalonia.Base/Animation/Animators/Int16Animator.cs diff --git a/src/Avalonia.Animation/Animators/Int32Animator.cs b/src/Avalonia.Base/Animation/Animators/Int32Animator.cs similarity index 100% rename from src/Avalonia.Animation/Animators/Int32Animator.cs rename to src/Avalonia.Base/Animation/Animators/Int32Animator.cs diff --git a/src/Avalonia.Animation/Animators/Int64Animator.cs b/src/Avalonia.Base/Animation/Animators/Int64Animator.cs similarity index 100% rename from src/Avalonia.Animation/Animators/Int64Animator.cs rename to src/Avalonia.Base/Animation/Animators/Int64Animator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/PointAnimator.cs b/src/Avalonia.Base/Animation/Animators/PointAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/PointAnimator.cs rename to src/Avalonia.Base/Animation/Animators/PointAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/RectAnimator.cs b/src/Avalonia.Base/Animation/Animators/RectAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/RectAnimator.cs rename to src/Avalonia.Base/Animation/Animators/RectAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs b/src/Avalonia.Base/Animation/Animators/RelativePointAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/RelativePointAnimator.cs rename to src/Avalonia.Base/Animation/Animators/RelativePointAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/SizeAnimator.cs b/src/Avalonia.Base/Animation/Animators/SizeAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/SizeAnimator.cs rename to src/Avalonia.Base/Animation/Animators/SizeAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/SolidColorBrushAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs rename to src/Avalonia.Base/Animation/Animators/SolidColorBrushAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/ThicknessAnimator.cs b/src/Avalonia.Base/Animation/Animators/ThicknessAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/ThicknessAnimator.cs rename to src/Avalonia.Base/Animation/Animators/ThicknessAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs rename to src/Avalonia.Base/Animation/Animators/TransformAnimator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs b/src/Avalonia.Base/Animation/Animators/TransformOperationsAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs rename to src/Avalonia.Base/Animation/Animators/TransformOperationsAnimator.cs diff --git a/src/Avalonia.Animation/Animators/UInt16Animator.cs b/src/Avalonia.Base/Animation/Animators/UInt16Animator.cs similarity index 100% rename from src/Avalonia.Animation/Animators/UInt16Animator.cs rename to src/Avalonia.Base/Animation/Animators/UInt16Animator.cs diff --git a/src/Avalonia.Animation/Animators/UInt32Animator.cs b/src/Avalonia.Base/Animation/Animators/UInt32Animator.cs similarity index 100% rename from src/Avalonia.Animation/Animators/UInt32Animator.cs rename to src/Avalonia.Base/Animation/Animators/UInt32Animator.cs diff --git a/src/Avalonia.Animation/Animators/UInt64Animator.cs b/src/Avalonia.Base/Animation/Animators/UInt64Animator.cs similarity index 100% rename from src/Avalonia.Animation/Animators/UInt64Animator.cs rename to src/Avalonia.Base/Animation/Animators/UInt64Animator.cs diff --git a/src/Avalonia.Visuals/Animation/Animators/VectorAnimator.cs b/src/Avalonia.Base/Animation/Animators/VectorAnimator.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Animators/VectorAnimator.cs rename to src/Avalonia.Base/Animation/Animators/VectorAnimator.cs diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Base/Animation/Clock.cs similarity index 100% rename from src/Avalonia.Animation/Clock.cs rename to src/Avalonia.Base/Animation/Clock.cs diff --git a/src/Avalonia.Animation/ClockBase.cs b/src/Avalonia.Base/Animation/ClockBase.cs similarity index 100% rename from src/Avalonia.Animation/ClockBase.cs rename to src/Avalonia.Base/Animation/ClockBase.cs diff --git a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs b/src/Avalonia.Base/Animation/CompositePageTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/CompositePageTransition.cs rename to src/Avalonia.Base/Animation/CompositePageTransition.cs diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Base/Animation/CrossFade.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/CrossFade.cs rename to src/Avalonia.Base/Animation/CrossFade.cs diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Base/Animation/Cue.cs similarity index 100% rename from src/Avalonia.Animation/Cue.cs rename to src/Avalonia.Base/Animation/Cue.cs diff --git a/src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs b/src/Avalonia.Base/Animation/DisposeAnimationInstanceSubject.cs similarity index 100% rename from src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs rename to src/Avalonia.Base/Animation/DisposeAnimationInstanceSubject.cs diff --git a/src/Avalonia.Animation/Easing/BackEaseIn.cs b/src/Avalonia.Base/Animation/Easings/BackEaseIn.cs similarity index 100% rename from src/Avalonia.Animation/Easing/BackEaseIn.cs rename to src/Avalonia.Base/Animation/Easings/BackEaseIn.cs diff --git a/src/Avalonia.Animation/Easing/BackEaseInOut.cs b/src/Avalonia.Base/Animation/Easings/BackEaseInOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/BackEaseInOut.cs rename to src/Avalonia.Base/Animation/Easings/BackEaseInOut.cs diff --git a/src/Avalonia.Animation/Easing/BackEaseOut.cs b/src/Avalonia.Base/Animation/Easings/BackEaseOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/BackEaseOut.cs rename to src/Avalonia.Base/Animation/Easings/BackEaseOut.cs diff --git a/src/Avalonia.Animation/Easing/BounceEaseIn.cs b/src/Avalonia.Base/Animation/Easings/BounceEaseIn.cs similarity index 100% rename from src/Avalonia.Animation/Easing/BounceEaseIn.cs rename to src/Avalonia.Base/Animation/Easings/BounceEaseIn.cs diff --git a/src/Avalonia.Animation/Easing/BounceEaseInOut.cs b/src/Avalonia.Base/Animation/Easings/BounceEaseInOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/BounceEaseInOut.cs rename to src/Avalonia.Base/Animation/Easings/BounceEaseInOut.cs diff --git a/src/Avalonia.Animation/Easing/BounceEaseOut.cs b/src/Avalonia.Base/Animation/Easings/BounceEaseOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/BounceEaseOut.cs rename to src/Avalonia.Base/Animation/Easings/BounceEaseOut.cs diff --git a/src/Avalonia.Animation/Easing/CircularEaseIn.cs b/src/Avalonia.Base/Animation/Easings/CircularEaseIn.cs similarity index 100% rename from src/Avalonia.Animation/Easing/CircularEaseIn.cs rename to src/Avalonia.Base/Animation/Easings/CircularEaseIn.cs diff --git a/src/Avalonia.Animation/Easing/CircularEaseInOut.cs b/src/Avalonia.Base/Animation/Easings/CircularEaseInOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/CircularEaseInOut.cs rename to src/Avalonia.Base/Animation/Easings/CircularEaseInOut.cs diff --git a/src/Avalonia.Animation/Easing/CircularEaseOut.cs b/src/Avalonia.Base/Animation/Easings/CircularEaseOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/CircularEaseOut.cs rename to src/Avalonia.Base/Animation/Easings/CircularEaseOut.cs diff --git a/src/Avalonia.Animation/Easing/CubicEaseIn.cs b/src/Avalonia.Base/Animation/Easings/CubicEaseIn.cs similarity index 100% rename from src/Avalonia.Animation/Easing/CubicEaseIn.cs rename to src/Avalonia.Base/Animation/Easings/CubicEaseIn.cs diff --git a/src/Avalonia.Animation/Easing/CubicEaseInOut.cs b/src/Avalonia.Base/Animation/Easings/CubicEaseInOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/CubicEaseInOut.cs rename to src/Avalonia.Base/Animation/Easings/CubicEaseInOut.cs diff --git a/src/Avalonia.Animation/Easing/CubicEaseOut.cs b/src/Avalonia.Base/Animation/Easings/CubicEaseOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/CubicEaseOut.cs rename to src/Avalonia.Base/Animation/Easings/CubicEaseOut.cs diff --git a/src/Avalonia.Base/Animation/Easings/Easing.cs b/src/Avalonia.Base/Animation/Easings/Easing.cs new file mode 100644 index 0000000000..d4f817ccc3 --- /dev/null +++ b/src/Avalonia.Base/Animation/Easings/Easing.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Avalonia.SourceGenerator; + +namespace Avalonia.Animation.Easings +{ + /// + /// Base class for all Easing classes. + /// + [TypeConverter(typeof(EasingTypeConverter))] + public abstract partial class Easing : IEasing + { + /// + public abstract double Ease(double progress); + + private const string Namespace = "Avalonia.Animation.Easings"; + + [SubtypesFactory(typeof(Easing), Namespace)] + private static partial bool TryCreateEasingInstance(string type, [NotNullWhen(true)] out Easing? instance); + + /// + /// Parses a Easing type string. + /// + /// The Easing type string. + /// Returns the instance of the parsed type. + public static Easing Parse(string e) + { +#if NETSTANDARD2_0 + if (e.Contains(",")) +#else + if (e.Contains(',')) +#endif + { + return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture)); + } + + return TryCreateEasingInstance(e, out var easing) + ? easing + : throw new FormatException($"Easing \"{e}\" was not found in {Namespace} namespace."); + } + } +} diff --git a/src/Avalonia.Animation/Easing/EasingTypeConverter.cs b/src/Avalonia.Base/Animation/Easings/EasingTypeConverter.cs similarity index 100% rename from src/Avalonia.Animation/Easing/EasingTypeConverter.cs rename to src/Avalonia.Base/Animation/Easings/EasingTypeConverter.cs diff --git a/src/Avalonia.Animation/Easing/ElasticEaseIn.cs b/src/Avalonia.Base/Animation/Easings/ElasticEaseIn.cs similarity index 100% rename from src/Avalonia.Animation/Easing/ElasticEaseIn.cs rename to src/Avalonia.Base/Animation/Easings/ElasticEaseIn.cs diff --git a/src/Avalonia.Animation/Easing/ElasticEaseInOut.cs b/src/Avalonia.Base/Animation/Easings/ElasticEaseInOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/ElasticEaseInOut.cs rename to src/Avalonia.Base/Animation/Easings/ElasticEaseInOut.cs diff --git a/src/Avalonia.Animation/Easing/ElasticEaseOut.cs b/src/Avalonia.Base/Animation/Easings/ElasticEaseOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/ElasticEaseOut.cs rename to src/Avalonia.Base/Animation/Easings/ElasticEaseOut.cs diff --git a/src/Avalonia.Animation/Easing/ExponentialEaseIn.cs b/src/Avalonia.Base/Animation/Easings/ExponentialEaseIn.cs similarity index 100% rename from src/Avalonia.Animation/Easing/ExponentialEaseIn.cs rename to src/Avalonia.Base/Animation/Easings/ExponentialEaseIn.cs diff --git a/src/Avalonia.Animation/Easing/ExponentialEaseInOut.cs b/src/Avalonia.Base/Animation/Easings/ExponentialEaseInOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/ExponentialEaseInOut.cs rename to src/Avalonia.Base/Animation/Easings/ExponentialEaseInOut.cs diff --git a/src/Avalonia.Animation/Easing/ExponentialEaseOut.cs b/src/Avalonia.Base/Animation/Easings/ExponentialEaseOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/ExponentialEaseOut.cs rename to src/Avalonia.Base/Animation/Easings/ExponentialEaseOut.cs diff --git a/src/Avalonia.Animation/Easing/IEasing.cs b/src/Avalonia.Base/Animation/Easings/IEasing.cs similarity index 100% rename from src/Avalonia.Animation/Easing/IEasing.cs rename to src/Avalonia.Base/Animation/Easings/IEasing.cs diff --git a/src/Avalonia.Animation/Easing/LinearEasing.cs b/src/Avalonia.Base/Animation/Easings/LinearEasing.cs similarity index 100% rename from src/Avalonia.Animation/Easing/LinearEasing.cs rename to src/Avalonia.Base/Animation/Easings/LinearEasing.cs diff --git a/src/Avalonia.Animation/Easing/QuadraticEaseIn.cs b/src/Avalonia.Base/Animation/Easings/QuadraticEaseIn.cs similarity index 100% rename from src/Avalonia.Animation/Easing/QuadraticEaseIn.cs rename to src/Avalonia.Base/Animation/Easings/QuadraticEaseIn.cs diff --git a/src/Avalonia.Animation/Easing/QuadraticEaseInOut.cs b/src/Avalonia.Base/Animation/Easings/QuadraticEaseInOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/QuadraticEaseInOut.cs rename to src/Avalonia.Base/Animation/Easings/QuadraticEaseInOut.cs diff --git a/src/Avalonia.Animation/Easing/QuadraticEaseOut.cs b/src/Avalonia.Base/Animation/Easings/QuadraticEaseOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/QuadraticEaseOut.cs rename to src/Avalonia.Base/Animation/Easings/QuadraticEaseOut.cs diff --git a/src/Avalonia.Animation/Easing/QuarticEaseIn.cs b/src/Avalonia.Base/Animation/Easings/QuarticEaseIn.cs similarity index 100% rename from src/Avalonia.Animation/Easing/QuarticEaseIn.cs rename to src/Avalonia.Base/Animation/Easings/QuarticEaseIn.cs diff --git a/src/Avalonia.Animation/Easing/QuarticEaseInOut.cs b/src/Avalonia.Base/Animation/Easings/QuarticEaseInOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/QuarticEaseInOut.cs rename to src/Avalonia.Base/Animation/Easings/QuarticEaseInOut.cs diff --git a/src/Avalonia.Animation/Easing/QuarticEaseOut.cs b/src/Avalonia.Base/Animation/Easings/QuarticEaseOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/QuarticEaseOut.cs rename to src/Avalonia.Base/Animation/Easings/QuarticEaseOut.cs diff --git a/src/Avalonia.Animation/Easing/QuinticEaseIn.cs b/src/Avalonia.Base/Animation/Easings/QuinticEaseIn.cs similarity index 100% rename from src/Avalonia.Animation/Easing/QuinticEaseIn.cs rename to src/Avalonia.Base/Animation/Easings/QuinticEaseIn.cs diff --git a/src/Avalonia.Animation/Easing/QuinticEaseInOut.cs b/src/Avalonia.Base/Animation/Easings/QuinticEaseInOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/QuinticEaseInOut.cs rename to src/Avalonia.Base/Animation/Easings/QuinticEaseInOut.cs diff --git a/src/Avalonia.Animation/Easing/QuinticEaseOut.cs b/src/Avalonia.Base/Animation/Easings/QuinticEaseOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/QuinticEaseOut.cs rename to src/Avalonia.Base/Animation/Easings/QuinticEaseOut.cs diff --git a/src/Avalonia.Animation/Easing/SineEaseIn.cs b/src/Avalonia.Base/Animation/Easings/SineEaseIn.cs similarity index 100% rename from src/Avalonia.Animation/Easing/SineEaseIn.cs rename to src/Avalonia.Base/Animation/Easings/SineEaseIn.cs diff --git a/src/Avalonia.Animation/Easing/SineEaseInOut.cs b/src/Avalonia.Base/Animation/Easings/SineEaseInOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/SineEaseInOut.cs rename to src/Avalonia.Base/Animation/Easings/SineEaseInOut.cs diff --git a/src/Avalonia.Animation/Easing/SineEaseOut.cs b/src/Avalonia.Base/Animation/Easings/SineEaseOut.cs similarity index 100% rename from src/Avalonia.Animation/Easing/SineEaseOut.cs rename to src/Avalonia.Base/Animation/Easings/SineEaseOut.cs diff --git a/src/Avalonia.Animation/Easing/SplineEasing.cs b/src/Avalonia.Base/Animation/Easings/SplineEasing.cs similarity index 100% rename from src/Avalonia.Animation/Easing/SplineEasing.cs rename to src/Avalonia.Base/Animation/Easings/SplineEasing.cs diff --git a/src/Avalonia.Animation/FillMode.cs b/src/Avalonia.Base/Animation/FillMode.cs similarity index 100% rename from src/Avalonia.Animation/FillMode.cs rename to src/Avalonia.Base/Animation/FillMode.cs diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Base/Animation/IAnimation.cs similarity index 100% rename from src/Avalonia.Animation/IAnimation.cs rename to src/Avalonia.Base/Animation/IAnimation.cs diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Base/Animation/IAnimationSetter.cs similarity index 100% rename from src/Avalonia.Animation/IAnimationSetter.cs rename to src/Avalonia.Base/Animation/IAnimationSetter.cs diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Base/Animation/IAnimator.cs similarity index 100% rename from src/Avalonia.Animation/IAnimator.cs rename to src/Avalonia.Base/Animation/IAnimator.cs diff --git a/src/Avalonia.Animation/IClock.cs b/src/Avalonia.Base/Animation/IClock.cs similarity index 100% rename from src/Avalonia.Animation/IClock.cs rename to src/Avalonia.Base/Animation/IClock.cs diff --git a/src/Avalonia.Animation/IGlobalClock.cs b/src/Avalonia.Base/Animation/IGlobalClock.cs similarity index 100% rename from src/Avalonia.Animation/IGlobalClock.cs rename to src/Avalonia.Base/Animation/IGlobalClock.cs diff --git a/src/Avalonia.Visuals/Animation/IPageTransition.cs b/src/Avalonia.Base/Animation/IPageTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/IPageTransition.cs rename to src/Avalonia.Base/Animation/IPageTransition.cs diff --git a/src/Avalonia.Animation/ITransition.cs b/src/Avalonia.Base/Animation/ITransition.cs similarity index 100% rename from src/Avalonia.Animation/ITransition.cs rename to src/Avalonia.Base/Animation/ITransition.cs diff --git a/src/Avalonia.Animation/IterationCount.cs b/src/Avalonia.Base/Animation/IterationCount.cs similarity index 100% rename from src/Avalonia.Animation/IterationCount.cs rename to src/Avalonia.Base/Animation/IterationCount.cs diff --git a/src/Avalonia.Animation/IterationCountTypeConverter.cs b/src/Avalonia.Base/Animation/IterationCountTypeConverter.cs similarity index 100% rename from src/Avalonia.Animation/IterationCountTypeConverter.cs rename to src/Avalonia.Base/Animation/IterationCountTypeConverter.cs diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Base/Animation/KeyFrame.cs similarity index 100% rename from src/Avalonia.Animation/KeyFrame.cs rename to src/Avalonia.Base/Animation/KeyFrame.cs diff --git a/src/Avalonia.Animation/KeyFrames.cs b/src/Avalonia.Base/Animation/KeyFrames.cs similarity index 100% rename from src/Avalonia.Animation/KeyFrames.cs rename to src/Avalonia.Base/Animation/KeyFrames.cs diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Base/Animation/KeySpline.cs similarity index 100% rename from src/Avalonia.Animation/KeySpline.cs rename to src/Avalonia.Base/Animation/KeySpline.cs diff --git a/src/Avalonia.Animation/KeySplineTypeConverter.cs b/src/Avalonia.Base/Animation/KeySplineTypeConverter.cs similarity index 100% rename from src/Avalonia.Animation/KeySplineTypeConverter.cs rename to src/Avalonia.Base/Animation/KeySplineTypeConverter.cs diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Base/Animation/PageSlide.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/PageSlide.cs rename to src/Avalonia.Base/Animation/PageSlide.cs diff --git a/src/Avalonia.Animation/PlayState.cs b/src/Avalonia.Base/Animation/PlayState.cs similarity index 100% rename from src/Avalonia.Animation/PlayState.cs rename to src/Avalonia.Base/Animation/PlayState.cs diff --git a/src/Avalonia.Animation/PlaybackDirection.cs b/src/Avalonia.Base/Animation/PlaybackDirection.cs similarity index 100% rename from src/Avalonia.Animation/PlaybackDirection.cs rename to src/Avalonia.Base/Animation/PlaybackDirection.cs diff --git a/src/Avalonia.Visuals/Animation/RenderLoopClock.cs b/src/Avalonia.Base/Animation/RenderLoopClock.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/RenderLoopClock.cs rename to src/Avalonia.Base/Animation/RenderLoopClock.cs diff --git a/src/Avalonia.Animation/Transition.cs b/src/Avalonia.Base/Animation/Transition.cs similarity index 100% rename from src/Avalonia.Animation/Transition.cs rename to src/Avalonia.Base/Animation/Transition.cs diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Base/Animation/TransitionInstance.cs similarity index 100% rename from src/Avalonia.Animation/TransitionInstance.cs rename to src/Avalonia.Base/Animation/TransitionInstance.cs diff --git a/src/Avalonia.Animation/TransitionObservableBase.cs b/src/Avalonia.Base/Animation/TransitionObservableBase.cs similarity index 100% rename from src/Avalonia.Animation/TransitionObservableBase.cs rename to src/Avalonia.Base/Animation/TransitionObservableBase.cs diff --git a/src/Avalonia.Animation/Transitions.cs b/src/Avalonia.Base/Animation/Transitions.cs similarity index 100% rename from src/Avalonia.Animation/Transitions.cs rename to src/Avalonia.Base/Animation/Transitions.cs diff --git a/src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs b/src/Avalonia.Base/Animation/Transitions/BoxShadowsTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Transitions/BoxShadowsTransition.cs rename to src/Avalonia.Base/Animation/Transitions/BoxShadowsTransition.cs diff --git a/src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs b/src/Avalonia.Base/Animation/Transitions/BrushTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Transitions/BrushTransition.cs rename to src/Avalonia.Base/Animation/Transitions/BrushTransition.cs diff --git a/src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs b/src/Avalonia.Base/Animation/Transitions/ColorTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Transitions/ColorTransition.cs rename to src/Avalonia.Base/Animation/Transitions/ColorTransition.cs diff --git a/src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs b/src/Avalonia.Base/Animation/Transitions/CornerRadiusTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs rename to src/Avalonia.Base/Animation/Transitions/CornerRadiusTransition.cs diff --git a/src/Avalonia.Animation/Transitions/DoubleTransition.cs b/src/Avalonia.Base/Animation/Transitions/DoubleTransition.cs similarity index 100% rename from src/Avalonia.Animation/Transitions/DoubleTransition.cs rename to src/Avalonia.Base/Animation/Transitions/DoubleTransition.cs diff --git a/src/Avalonia.Animation/Transitions/FloatTransition.cs b/src/Avalonia.Base/Animation/Transitions/FloatTransition.cs similarity index 100% rename from src/Avalonia.Animation/Transitions/FloatTransition.cs rename to src/Avalonia.Base/Animation/Transitions/FloatTransition.cs diff --git a/src/Avalonia.Animation/Transitions/IntegerTransition.cs b/src/Avalonia.Base/Animation/Transitions/IntegerTransition.cs similarity index 100% rename from src/Avalonia.Animation/Transitions/IntegerTransition.cs rename to src/Avalonia.Base/Animation/Transitions/IntegerTransition.cs diff --git a/src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs b/src/Avalonia.Base/Animation/Transitions/PointTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs rename to src/Avalonia.Base/Animation/Transitions/PointTransition.cs diff --git a/src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs b/src/Avalonia.Base/Animation/Transitions/RelativePointTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Transitions/RelativePointTransition.cs rename to src/Avalonia.Base/Animation/Transitions/RelativePointTransition.cs diff --git a/src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs b/src/Avalonia.Base/Animation/Transitions/SizeTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs rename to src/Avalonia.Base/Animation/Transitions/SizeTransition.cs diff --git a/src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs b/src/Avalonia.Base/Animation/Transitions/ThicknessTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs rename to src/Avalonia.Base/Animation/Transitions/ThicknessTransition.cs diff --git a/src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs b/src/Avalonia.Base/Animation/Transitions/TransformOperationsTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs rename to src/Avalonia.Base/Animation/Transitions/TransformOperationsTransition.cs diff --git a/src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs b/src/Avalonia.Base/Animation/Transitions/VectorTransition.cs similarity index 100% rename from src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs rename to src/Avalonia.Base/Animation/Transitions/VectorTransition.cs diff --git a/src/Avalonia.Animation/Utils/BounceEaseUtils.cs b/src/Avalonia.Base/Animation/Utils/BounceEaseUtils.cs similarity index 100% rename from src/Avalonia.Animation/Utils/BounceEaseUtils.cs rename to src/Avalonia.Base/Animation/Utils/BounceEaseUtils.cs diff --git a/src/Avalonia.Animation/Utils/EasingUtils.cs b/src/Avalonia.Base/Animation/Utils/EasingUtils.cs similarity index 100% rename from src/Avalonia.Animation/Utils/EasingUtils.cs rename to src/Avalonia.Base/Animation/Utils/EasingUtils.cs diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt index 4701a83175..7f378d2f65 100644 --- a/src/Avalonia.Base/ApiCompatBaseline.txt +++ b/src/Avalonia.Base/ApiCompatBaseline.txt @@ -1,3 +1,4 @@ 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: 1 +Total Issues: 2 diff --git a/src/Avalonia.Visuals/Assets/BiDi.trie b/src/Avalonia.Base/Assets/BiDi.trie similarity index 100% rename from src/Avalonia.Visuals/Assets/BiDi.trie rename to src/Avalonia.Base/Assets/BiDi.trie diff --git a/src/Avalonia.Visuals/Assets/GraphemeBreak.trie b/src/Avalonia.Base/Assets/GraphemeBreak.trie similarity index 100% rename from src/Avalonia.Visuals/Assets/GraphemeBreak.trie rename to src/Avalonia.Base/Assets/GraphemeBreak.trie diff --git a/src/Avalonia.Visuals/Assets/UnicodeData.trie b/src/Avalonia.Base/Assets/UnicodeData.trie similarity index 100% rename from src/Avalonia.Visuals/Assets/UnicodeData.trie rename to src/Avalonia.Base/Assets/UnicodeData.trie diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 376075df2a..8e4755b4b7 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -5,6 +5,9 @@ Avalonia True + + + @@ -12,4 +15,6 @@ + + diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 437339d3b2..1f14ddede4 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -5,6 +5,7 @@ using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.PropertyStore; +using Avalonia.Reactive; using Avalonia.Threading; namespace Avalonia @@ -15,13 +16,13 @@ namespace Avalonia /// /// This class is analogous to DependencyObject in WPF. /// - public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink + public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged { - private IAvaloniaObject? _inheritanceParent; + private AvaloniaObject? _inheritanceParent; private List? _directBindings; private PropertyChangedEventHandler? _inpcChanged; private EventHandler? _propertyChanged; - private List? _inheritanceChildren; + private List? _inheritanceChildren; private ValueStore? _values; private bool _batchUpdate; @@ -58,7 +59,7 @@ namespace Avalonia /// /// The inheritance parent. /// - protected IAvaloniaObject? InheritanceParent + protected AvaloniaObject? InheritanceParent { get { @@ -320,14 +321,14 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - public void SetValue( + public IDisposable? SetValue( AvaloniaProperty property, object? value, BindingPriority priority = BindingPriority.LocalValue) { property = property ?? throw new ArgumentNullException(nameof(property)); - property.RouteSetValue(this, value, priority); + return property.RouteSetValue(this, value, priority); } /// @@ -385,6 +386,26 @@ namespace Avalonia SetDirectValueUnchecked(property, value); } + /// + /// Binds a to an observable. + /// + /// The property. + /// The observable. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + public IDisposable Bind( + AvaloniaProperty property, + IObservable source, + BindingPriority priority = BindingPriority.LocalValue) + { + property = property ?? throw new ArgumentNullException(nameof(property)); + source = source ?? throw new ArgumentNullException(nameof(source)); + + return property.RouteBind(this, source.ToBindingValue(), priority); + } + /// /// Binds a to an observable. /// @@ -445,9 +466,8 @@ namespace Avalonia /// /// Coerces the specified . /// - /// The type of the property. /// The property. - public void CoerceValue(StyledPropertyBase property) + public void CoerceValue(AvaloniaProperty property) { _values?.CoerceValue(property); } @@ -475,19 +495,19 @@ namespace Avalonia } /// - void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child) + internal void AddInheritanceChild(AvaloniaObject child) { - _inheritanceChildren ??= new List(); + _inheritanceChildren ??= new List(); _inheritanceChildren.Add(child); } /// - void IAvaloniaObject.RemoveInheritanceChild(IAvaloniaObject child) + internal void RemoveInheritanceChild(AvaloniaObject child) { _inheritanceChildren?.Remove(child); } - void IAvaloniaObject.InheritedPropertyChanged( + internal void InheritedPropertyChanged( AvaloniaProperty property, Optional oldValue, Optional newValue) @@ -504,7 +524,7 @@ namespace Avalonia return _propertyChanged?.GetInvocationList(); } - void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) + internal void ValueChanged(AvaloniaPropertyChangedEventArgs change) { var property = (StyledPropertyBase)change.Property; @@ -543,7 +563,7 @@ namespace Avalonia } } - void IValueSink.Completed( + internal void Completed( StyledPropertyBase property, IPriorityValueEntry entry, Optional oldValue) @@ -554,7 +574,7 @@ namespace Avalonia oldValue, default, BindingPriority.Unset); - ((IValueSink)this).ValueChanged(change); + ValueChanged(change); } /// @@ -565,14 +585,11 @@ namespace Avalonia /// The old inheritance parent. internal void InheritanceParentChanged( StyledPropertyBase property, - IAvaloniaObject? oldParent) + AvaloniaObject? oldParent) { - var oldValue = oldParent switch - { - AvaloniaObject o => o.GetValueOrInheritedOrDefault(property), - null => property.GetDefaultValue(GetType()), - _ => oldParent.GetValue(property) - }; + var oldValue = oldParent is not null ? + oldParent.GetValueOrInheritedOrDefault(property) : + property.GetDefaultValue(GetType()); var newValue = GetInheritedOrDefault(property); @@ -629,10 +646,12 @@ namespace Avalonia /// enabled. /// /// The property. - /// The new binding value for the property. - protected virtual void UpdateDataValidation( - AvaloniaProperty property, - BindingValue value) + /// The current data binding state. + /// The current data binding error, if any. + protected virtual void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { } @@ -640,7 +659,7 @@ namespace Avalonia /// Called when a avalonia property changes on the object. /// /// The property change details. - protected virtual void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) + protected virtual void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) { if (change.IsEffectiveValueChange) { @@ -652,7 +671,7 @@ namespace Avalonia /// Called when a avalonia property changes on the object. /// /// The property change details. - protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { } @@ -843,7 +862,7 @@ namespace Avalonia if (metadata.EnableDataValidation == true) { - UpdateDataValidation(property, value); + UpdateDataValidation(property, value.Type, value.Error); } } diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 51237409bf..134e3b2ac7 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -25,7 +25,7 @@ namespace Avalonia } /// - /// Gets an observable for a . + /// Gets an observable for an . /// /// The object. /// The property. @@ -44,7 +44,7 @@ namespace Avalonia } /// - /// Gets an observable for a . + /// Gets an observable for an . /// /// The object. /// The property type. @@ -64,7 +64,7 @@ namespace Avalonia } /// - /// Gets an observable for a . + /// Gets an observable for an . /// /// The object. /// The property. @@ -85,7 +85,7 @@ namespace Avalonia } /// - /// Gets an observable for a . + /// Gets an observable for an . /// /// The object. /// The property type. @@ -128,7 +128,7 @@ namespace Avalonia } /// - /// Gets a subject for a . + /// Gets a subject for an . /// /// The object. /// The property. @@ -150,7 +150,7 @@ namespace Avalonia } /// - /// Gets a subject for a . + /// Gets a subject for an . /// /// The property type. /// The object. @@ -230,30 +230,7 @@ namespace Avalonia } /// - /// Binds a to an observable. - /// - /// The object. - /// The property. - /// The observable. - /// The priority of the binding. - /// - /// A disposable which can be used to terminate the binding. - /// - public static IDisposable Bind( - this IAvaloniaObject target, - AvaloniaProperty property, - IObservable> source, - BindingPriority priority = BindingPriority.LocalValue) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - property = property ?? throw new ArgumentNullException(nameof(property)); - source = source ?? throw new ArgumentNullException(nameof(source)); - - return property.RouteBind(target, source, priority); - } - - /// - /// Binds a to an observable. + /// Binds an to an observable. /// /// The type of the property. /// The object. @@ -273,42 +250,22 @@ namespace Avalonia property = property ?? throw new ArgumentNullException(nameof(property)); source = source ?? throw new ArgumentNullException(nameof(source)); - return property switch + if (target is AvaloniaObject ao) { - StyledPropertyBase styled => target.Bind(styled, source, priority), - DirectPropertyBase direct => target.Bind(direct, source), - _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), - }; - } + return property switch + { + StyledPropertyBase styled => ao.Bind(styled, source, priority), + DirectPropertyBase direct => ao.Bind(direct, source), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), + }; + } - /// - /// Binds a to an observable. - /// - /// The object. - /// The property. - /// The observable. - /// The priority of the binding. - /// - /// A disposable which can be used to terminate the binding. - /// - public static IDisposable Bind( - this IAvaloniaObject target, - AvaloniaProperty property, - IObservable source, - BindingPriority priority = BindingPriority.LocalValue) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - property = property ?? throw new ArgumentNullException(nameof(property)); - source = source ?? throw new ArgumentNullException(nameof(source)); + throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); - return target.Bind( - property, - source.ToBindingValue(), - priority); } /// - /// Binds a to an observable. + /// Binds an to an observable. /// /// The object. /// The property. @@ -334,7 +291,7 @@ namespace Avalonia } /// - /// Binds a property on an to an . + /// Binds a property on an to an . /// /// The object. /// The property to bind. @@ -374,56 +331,6 @@ namespace Avalonia } } - /// - /// Clears a 's local value. - /// - /// The object. - /// The property. - public static void ClearValue(this IAvaloniaObject target, AvaloniaProperty property) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - property = property ?? throw new ArgumentNullException(nameof(property)); - - property.RouteClearValue(target); - } - - /// - /// Clears a 's local value. - /// - /// The object. - /// The property. - public static void ClearValue(this IAvaloniaObject target, AvaloniaProperty property) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - property = property ?? throw new ArgumentNullException(nameof(property)); - - switch (property) - { - case StyledPropertyBase styled: - target.ClearValue(styled); - break; - case DirectPropertyBase direct: - target.ClearValue(direct); - break; - default: - throw new NotSupportedException("Unsupported AvaloniaProperty type."); - } - } - - /// - /// Gets a value. - /// - /// The object. - /// The property. - /// The value. - public static object? GetValue(this IAvaloniaObject target, AvaloniaProperty property) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - property = property ?? throw new ArgumentNullException(nameof(property)); - - return property.RouteGetValue(target); - } - /// /// Gets a value. /// @@ -436,12 +343,18 @@ namespace Avalonia target = target ?? throw new ArgumentNullException(nameof(target)); property = property ?? throw new ArgumentNullException(nameof(property)); - return property switch + if (target is AvaloniaObject ao) { - StyledPropertyBase styled => target.GetValue(styled), - DirectPropertyBase direct => target.GetValue(direct), - _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") - }; + return property switch + { + StyledPropertyBase styled => ao.GetValue(styled), + DirectPropertyBase direct => ao.GetValue(direct), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") + }; + + } + + throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); } /// @@ -456,7 +369,7 @@ namespace Avalonia /// . Note that this method does not return /// property values that come from inherited or default values. /// - /// For direct properties returns . + /// For direct properties returns the current value of the property. /// public static object? GetBaseValue( this IAvaloniaObject target, @@ -466,7 +379,9 @@ namespace Avalonia target = target ?? throw new ArgumentNullException(nameof(target)); property = property ?? throw new ArgumentNullException(nameof(property)); - return property.RouteGetBaseValue(target, maxPriority); + if (target is AvaloniaObject ao) + return property.RouteGetBaseValue(ao, maxPriority); + throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); } /// @@ -481,8 +396,7 @@ namespace Avalonia /// . Note that this method does not return property values /// that come from inherited or default values. /// - /// For direct properties returns - /// . + /// For direct properties returns the current value of the property. /// public static Optional GetBaseValue( this IAvaloniaObject target, @@ -492,69 +406,18 @@ namespace Avalonia target = target ?? throw new ArgumentNullException(nameof(target)); property = property ?? throw new ArgumentNullException(nameof(property)); - target = target ?? throw new ArgumentNullException(nameof(target)); - property = property ?? throw new ArgumentNullException(nameof(property)); - - return property switch + if (target is AvaloniaObject ao) { - StyledPropertyBase styled => target.GetBaseValue(styled, maxPriority), - DirectPropertyBase direct => target.GetValue(direct), - _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") - }; - } - - /// - /// Sets a value. - /// - /// The object. - /// The property. - /// The value. - /// The priority of the value. - /// - /// An if setting the property can be undone, otherwise null. - /// - public static IDisposable? SetValue( - this IAvaloniaObject target, - AvaloniaProperty property, - object? value, - BindingPriority priority = BindingPriority.LocalValue) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - property = property ?? throw new ArgumentNullException(nameof(property)); - - return property.RouteSetValue(target, value, priority); - } - - /// - /// Sets a value. - /// - /// The type of the property. - /// The object. - /// The property. - /// The value. - /// The priority of the value. - /// - /// An if setting the property can be undone, otherwise null. - /// - public static IDisposable? SetValue( - this IAvaloniaObject target, - AvaloniaProperty property, - T value, - BindingPriority priority = BindingPriority.LocalValue) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - property = property ?? throw new ArgumentNullException(nameof(property)); + return property switch + { + StyledPropertyBase styled => ao.GetBaseValue(styled, maxPriority), + DirectPropertyBase direct => ao.GetValue(direct), + _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") + }; - switch (property) - { - case StyledPropertyBase styled: - return target.SetValue(styled, value, priority); - case DirectPropertyBase direct: - target.SetValue(direct, value); - return null; - default: - throw new NotSupportedException("Unsupported AvaloniaProperty type."); } + + throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); } /// @@ -622,17 +485,6 @@ namespace Avalonia return observable.Subscribe(e => SubscribeAdapter(e, handler)); } - /// - /// Gets a description of a property that van be used in observables. - /// - /// The object. - /// The property - /// The description. - private static string GetDescription(IAvaloniaObject o, AvaloniaProperty property) - { - return $"{o.GetType().Name}.{property.Name}"; - } - /// /// Observer method for . diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 3d1e898f50..fd43ced196 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Styling; using Avalonia.Utilities; namespace Avalonia @@ -454,33 +455,24 @@ namespace Avalonia return Name; } - /// - /// Uses the visitor pattern to resolve an untyped property to a typed property. - /// - /// The type of user data passed. - /// The visitor which will accept the typed property. - /// The user data to pass. - public abstract void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) - where TData : struct; - /// /// Routes an untyped ClearValue call to a typed call. /// /// The object instance. - internal abstract void RouteClearValue(IAvaloniaObject o); + internal abstract void RouteClearValue(AvaloniaObject o); /// /// Routes an untyped GetValue call to a typed call. /// /// The object instance. - internal abstract object? RouteGetValue(IAvaloniaObject o); + internal abstract object? RouteGetValue(AvaloniaObject o); /// /// Routes an untyped GetBaseValue call to a typed call. /// /// The object instance. /// The maximum priority for the value. - internal abstract object? RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority); + internal abstract object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority); /// /// Routes an untyped SetValue call to a typed call. @@ -492,7 +484,7 @@ namespace Avalonia /// An if setting the property can be undone, otherwise null. /// internal abstract IDisposable? RouteSetValue( - IAvaloniaObject o, + AvaloniaObject o, object? value, BindingPriority priority); @@ -503,11 +495,12 @@ namespace Avalonia /// The binding source. /// The priority. internal abstract IDisposable RouteBind( - IAvaloniaObject o, + AvaloniaObject o, IObservable> source, BindingPriority priority); - internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject? oldParent); + internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent); + internal abstract ISetterInstance CreateSetterInstance(IStyleable target, object? value); /// /// Overrides the metadata for the property on the specified type. diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedExtensions.cs b/src/Avalonia.Base/AvaloniaPropertyChangedExtensions.cs new file mode 100644 index 0000000000..a52d6d15ab --- /dev/null +++ b/src/Avalonia.Base/AvaloniaPropertyChangedExtensions.cs @@ -0,0 +1,43 @@ +namespace Avalonia +{ + /// + /// Provides extensions for . + /// + public static class AvaloniaPropertyChangedExtensions + { + /// + /// Gets a typed value from . + /// + /// The value type. + /// The event args. + /// The value. + public static T GetOldValue(this AvaloniaPropertyChangedEventArgs e) + { + return ((AvaloniaPropertyChangedEventArgs)e).OldValue.GetValueOrDefault()!; + } + + /// + /// Gets a typed value from . + /// + /// The value type. + /// The event args. + /// The value. + public static T GetNewValue(this AvaloniaPropertyChangedEventArgs e) + { + return ((AvaloniaPropertyChangedEventArgs)e).NewValue.GetValueOrDefault()!; + } + + /// + /// Gets a typed value from and + /// . + /// + /// The value type. + /// The event args. + /// The value. + public static (T oldValue, T newValue) GetOldAndNewValue(this AvaloniaPropertyChangedEventArgs e) + { + var ev = (AvaloniaPropertyChangedEventArgs)e; + return (ev.OldValue.GetValueOrDefault()!, ev.NewValue.GetValueOrDefault()!); + } + } +} diff --git a/src/Avalonia.Visuals/AvaloniaPropertyExtensions.cs b/src/Avalonia.Base/AvaloniaPropertyExtensions.cs similarity index 100% rename from src/Avalonia.Visuals/AvaloniaPropertyExtensions.cs rename to src/Avalonia.Base/AvaloniaPropertyExtensions.cs diff --git a/src/Avalonia.Styling/ClassBindingManager.cs b/src/Avalonia.Base/ClassBindingManager.cs similarity index 100% rename from src/Avalonia.Styling/ClassBindingManager.cs rename to src/Avalonia.Base/ClassBindingManager.cs diff --git a/src/Avalonia.Visuals/Media/CombinedGeometry.cs b/src/Avalonia.Base/CombinedGeometry.cs similarity index 100% rename from src/Avalonia.Visuals/Media/CombinedGeometry.cs rename to src/Avalonia.Base/CombinedGeometry.cs diff --git a/src/Avalonia.Base/Controls/ChildNameScope.cs b/src/Avalonia.Base/Controls/ChildNameScope.cs new file mode 100644 index 0000000000..1ceaff924c --- /dev/null +++ b/src/Avalonia.Base/Controls/ChildNameScope.cs @@ -0,0 +1,73 @@ +using System.Threading.Tasks; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + public class ChildNameScope : INameScope + { + private readonly INameScope _parentScope; + private readonly NameScope _inner = new NameScope(); + + public ChildNameScope(INameScope parentScope) + { + _parentScope = parentScope; + } + + public void Register(string name, object element) => _inner.Register(name, element); + + public SynchronousCompletionAsyncResult FindAsync(string name) + { + var found = Find(name); + if (found != null) + return new SynchronousCompletionAsyncResult(found); + // Not found and both current and parent scope are in completed state + if(IsCompleted) + return new SynchronousCompletionAsyncResult((object?)null); + return DoFindAsync(name); + } + + public SynchronousCompletionAsyncResult DoFindAsync(string name) + { + var src = new SynchronousCompletionAsyncResultSource(); + + void ParentSearch() + { + var parentSearch = _parentScope.FindAsync(name); + if (parentSearch.IsCompleted) + src.SetResult(parentSearch.GetResult()); + else + parentSearch.OnCompleted(() => src.SetResult(parentSearch.GetResult())); + } + if (!_inner.IsCompleted) + { + // Guaranteed to be incomplete at this point + var innerSearch = _inner.FindAsync(name); + innerSearch.OnCompleted(() => + { + var value = innerSearch.GetResult(); + if (value != null) + src.SetResult(value); + else ParentSearch(); + }); + } + else + ParentSearch(); + + return src.AsyncResult; + } + + public object? Find(string name) + { + var found = _inner.Find(name); + if (found != null) + return found; + if (_inner.IsCompleted) + return _parentScope.Find(name); + return null; + } + + public void Complete() => _inner.Complete(); + + public bool IsCompleted => _inner.IsCompleted && _parentScope.IsCompleted; + } +} diff --git a/src/Avalonia.Styling/Controls/Classes.cs b/src/Avalonia.Base/Controls/Classes.cs similarity index 100% rename from src/Avalonia.Styling/Controls/Classes.cs rename to src/Avalonia.Base/Controls/Classes.cs diff --git a/src/Avalonia.Styling/Controls/INameScope.cs b/src/Avalonia.Base/Controls/INameScope.cs similarity index 100% rename from src/Avalonia.Styling/Controls/INameScope.cs rename to src/Avalonia.Base/Controls/INameScope.cs diff --git a/src/Avalonia.Styling/Controls/IPseudoClasses.cs b/src/Avalonia.Base/Controls/IPseudoClasses.cs similarity index 100% rename from src/Avalonia.Styling/Controls/IPseudoClasses.cs rename to src/Avalonia.Base/Controls/IPseudoClasses.cs diff --git a/src/Avalonia.Styling/Controls/IResourceDictionary.cs b/src/Avalonia.Base/Controls/IResourceDictionary.cs similarity index 100% rename from src/Avalonia.Styling/Controls/IResourceDictionary.cs rename to src/Avalonia.Base/Controls/IResourceDictionary.cs diff --git a/src/Avalonia.Styling/Controls/IResourceHost.cs b/src/Avalonia.Base/Controls/IResourceHost.cs similarity index 100% rename from src/Avalonia.Styling/Controls/IResourceHost.cs rename to src/Avalonia.Base/Controls/IResourceHost.cs diff --git a/src/Avalonia.Styling/Controls/IResourceNode.cs b/src/Avalonia.Base/Controls/IResourceNode.cs similarity index 100% rename from src/Avalonia.Styling/Controls/IResourceNode.cs rename to src/Avalonia.Base/Controls/IResourceNode.cs diff --git a/src/Avalonia.Styling/Controls/IResourceProvider.cs b/src/Avalonia.Base/Controls/IResourceProvider.cs similarity index 100% rename from src/Avalonia.Styling/Controls/IResourceProvider.cs rename to src/Avalonia.Base/Controls/IResourceProvider.cs diff --git a/src/Avalonia.Styling/Controls/ISetInheritanceParent.cs b/src/Avalonia.Base/Controls/ISetInheritanceParent.cs similarity index 100% rename from src/Avalonia.Styling/Controls/ISetInheritanceParent.cs rename to src/Avalonia.Base/Controls/ISetInheritanceParent.cs diff --git a/src/Avalonia.Styling/Controls/ISetLogicalParent.cs b/src/Avalonia.Base/Controls/ISetLogicalParent.cs similarity index 100% rename from src/Avalonia.Styling/Controls/ISetLogicalParent.cs rename to src/Avalonia.Base/Controls/ISetLogicalParent.cs diff --git a/src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs b/src/Avalonia.Base/Controls/Metadata/PseudoClassesAttribute.cs similarity index 100% rename from src/Avalonia.Styling/Controls/Metadata/PseudoClassesAttribute.cs rename to src/Avalonia.Base/Controls/Metadata/PseudoClassesAttribute.cs diff --git a/src/Avalonia.Styling/Controls/Metadata/TemplatePartAttribute.cs b/src/Avalonia.Base/Controls/Metadata/TemplatePartAttribute.cs similarity index 100% rename from src/Avalonia.Styling/Controls/Metadata/TemplatePartAttribute.cs rename to src/Avalonia.Base/Controls/Metadata/TemplatePartAttribute.cs diff --git a/src/Avalonia.Base/Controls/NameScope.cs b/src/Avalonia.Base/Controls/NameScope.cs new file mode 100644 index 0000000000..952401c00c --- /dev/null +++ b/src/Avalonia.Base/Controls/NameScope.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.LogicalTree; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements a name scope. + /// + public class NameScope : INameScope + { + /// + /// Defines the NameScope attached property. + /// + public static readonly AttachedProperty NameScopeProperty = + AvaloniaProperty.RegisterAttached("NameScope"); + + /// + public bool IsCompleted { get; private set; } + + private readonly Dictionary _inner = new Dictionary(); + + private readonly Dictionary> _pendingSearches = + new Dictionary>(); + + /// + /// Gets the value of the attached on a styled element. + /// + /// The styled element. + /// The value of the NameScope attached property. + public static INameScope GetNameScope(StyledElement styled) + { + _ = styled ?? throw new ArgumentNullException(nameof(styled)); + + return styled.GetValue(NameScopeProperty); + } + + /// + /// Sets the value of the attached on a styled element. + /// + /// The styled element. + /// The value to set. + public static void SetNameScope(StyledElement styled, INameScope value) + { + _ = styled ?? throw new ArgumentNullException(nameof(styled)); + + styled.SetValue(NameScopeProperty, value); + } + + /// + public void Register(string name, object element) + { + if (IsCompleted) + throw new InvalidOperationException("NameScope is completed, no further registrations are allowed"); + + _ = name ?? throw new ArgumentNullException(nameof(name)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + + if (_inner.TryGetValue(name, out var existing)) + { + if (existing != element) + { + throw new ArgumentException($"Control with the name '{name}' already registered."); + } + } + else + { + _inner.Add(name, element); + if (_pendingSearches.TryGetValue(name, out var tcs)) + { + _pendingSearches.Remove(name); + tcs.SetResult(element); + } + } + } + + public SynchronousCompletionAsyncResult FindAsync(string name) + { + var found = Find(name); + if (found != null) + return new SynchronousCompletionAsyncResult(found); + if (IsCompleted) + return new SynchronousCompletionAsyncResult((object?)null); + if (!_pendingSearches.TryGetValue(name, out var tcs)) + // We are intentionally running continuations synchronously here + _pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource(); + + return tcs.AsyncResult; + } + + /// + public object? Find(string name) + { + _ = name ?? throw new ArgumentNullException(nameof(name)); + + _inner.TryGetValue(name, out var result); + return result; + } + + public void Complete() + { + IsCompleted = true; + foreach (var kp in _pendingSearches) + kp.Value.TrySetResult(null); + _pendingSearches.Clear(); + } + + + } +} diff --git a/src/Avalonia.Styling/Controls/NameScopeEventArgs.cs b/src/Avalonia.Base/Controls/NameScopeEventArgs.cs similarity index 100% rename from src/Avalonia.Styling/Controls/NameScopeEventArgs.cs rename to src/Avalonia.Base/Controls/NameScopeEventArgs.cs diff --git a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs b/src/Avalonia.Base/Controls/NameScopeExtensions.cs similarity index 100% rename from src/Avalonia.Styling/Controls/NameScopeExtensions.cs rename to src/Avalonia.Base/Controls/NameScopeExtensions.cs diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Base/Controls/NameScopeLocator.cs similarity index 100% rename from src/Avalonia.Styling/Controls/NameScopeLocator.cs rename to src/Avalonia.Base/Controls/NameScopeLocator.cs diff --git a/src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs b/src/Avalonia.Base/Controls/PseudoClassesExtensions.cs similarity index 100% rename from src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs rename to src/Avalonia.Base/Controls/PseudoClassesExtensions.cs diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs similarity index 100% rename from src/Avalonia.Styling/Controls/ResourceDictionary.cs rename to src/Avalonia.Base/Controls/ResourceDictionary.cs diff --git a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs similarity index 100% rename from src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs rename to src/Avalonia.Base/Controls/ResourceNodeExtensions.cs diff --git a/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs b/src/Avalonia.Base/Controls/ResourcesChangedEventArgs.cs similarity index 100% rename from src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs rename to src/Avalonia.Base/Controls/ResourcesChangedEventArgs.cs diff --git a/src/Avalonia.Visuals/CornerRadius.cs b/src/Avalonia.Base/CornerRadius.cs similarity index 100% rename from src/Avalonia.Visuals/CornerRadius.cs rename to src/Avalonia.Base/CornerRadius.cs diff --git a/src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs b/src/Avalonia.Base/Diagnostics/StyleDiagnostics.cs similarity index 100% rename from src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs rename to src/Avalonia.Base/Diagnostics/StyleDiagnostics.cs diff --git a/src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs b/src/Avalonia.Base/Diagnostics/StyledElementExtensions.cs similarity index 100% rename from src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs rename to src/Avalonia.Base/Diagnostics/StyledElementExtensions.cs diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 9c4896b96c..9c1ffce24c 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Data; using Avalonia.Reactive; +using Avalonia.Styling; using Avalonia.Utilities; namespace Avalonia @@ -121,31 +122,25 @@ namespace Avalonia } /// - public override void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) - { - visitor.Visit(this, ref data); - } - - /// - internal override void RouteClearValue(IAvaloniaObject o) + internal override void RouteClearValue(AvaloniaObject o) { o.ClearValue(this); } /// - internal override object? RouteGetValue(IAvaloniaObject o) + internal override object? RouteGetValue(AvaloniaObject o) { return o.GetValue(this); } - internal override object? RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority) { return o.GetValue(this); } /// internal override IDisposable? RouteSetValue( - IAvaloniaObject o, + AvaloniaObject o, object? value, BindingPriority priority) { @@ -169,7 +164,7 @@ namespace Avalonia /// internal override IDisposable RouteBind( - IAvaloniaObject o, + AvaloniaObject o, IObservable> source, BindingPriority priority) { @@ -177,9 +172,34 @@ namespace Avalonia return o.Bind(this, adapter); } - internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject? oldParent) + internal override void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent) { throw new NotSupportedException("Direct properties do not support inheritance."); } + + internal override ISetterInstance CreateSetterInstance(IStyleable target, object? value) + { + if (value is IBinding binding) + { + return new PropertySetterBindingInstance( + target, + this, + binding); + } + else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType)) + { + return new PropertySetterLazyInstance( + target, + this, + () => (TValue)template.Build()); + } + else + { + return new PropertySetterInstance( + target, + this, + (TValue)value!); + } + } } } diff --git a/src/Avalonia.Visuals/Media/GeometryCollection.cs b/src/Avalonia.Base/GeometryCollection.cs similarity index 100% rename from src/Avalonia.Visuals/Media/GeometryCollection.cs rename to src/Avalonia.Base/GeometryCollection.cs diff --git a/src/Avalonia.Base/GeometryGroup.cs b/src/Avalonia.Base/GeometryGroup.cs new file mode 100644 index 0000000000..b90c9c6d8a --- /dev/null +++ b/src/Avalonia.Base/GeometryGroup.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Metadata; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Media +{ + /// + /// Represents a composite geometry, composed of other objects. + /// + public class GeometryGroup : Geometry + { + public static readonly DirectProperty ChildrenProperty = + AvaloniaProperty.RegisterDirect ( + nameof(Children), + o => o.Children, + (o, v) => o.Children = v); + + public static readonly StyledProperty FillRuleProperty = + AvaloniaProperty.Register(nameof(FillRule)); + + private GeometryCollection? _children; + private bool _childrenSet; + + /// + /// Gets or sets the collection that contains the child geometries. + /// + [Content] + public GeometryCollection? Children + { + get => _children ??= (!_childrenSet ? new GeometryCollection() : null); + set + { + SetAndRaise(ChildrenProperty, ref _children, value); + _childrenSet = true; + } + } + + /// + /// Gets or sets how the intersecting areas of the objects contained in this + /// are combined. The default is . + /// + public FillRule FillRule + { + get => GetValue(FillRuleProperty); + set => SetValue(FillRuleProperty, value); + } + + public override Geometry Clone() + { + var result = new GeometryGroup { FillRule = FillRule, Transform = Transform }; + if (_children?.Count > 0) + result.Children = new GeometryCollection(_children); + return result; + } + + protected override IGeometryImpl? CreateDefiningGeometry() + { + if (_children?.Count > 0) + { + var factory = AvaloniaLocator.Current.GetRequiredService(); + return factory.CreateGeometryGroup(FillRule, _children); + } + + return null; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ChildrenProperty || change.Property == FillRuleProperty) + { + InvalidateGeometry(); + } + } + } +} diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 654528658f..00f5062f9e 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -17,42 +17,14 @@ namespace Avalonia /// Clears an 's local value. /// /// The property. - void ClearValue(StyledPropertyBase property); - - /// - /// Clears an 's local value. - /// - /// The property. - void ClearValue(DirectPropertyBase property); - - /// - /// Gets a value. - /// - /// The type of the property. - /// The property. - /// The value. - T GetValue(StyledPropertyBase property); + void ClearValue(AvaloniaProperty property); /// /// Gets a value. /// - /// The type of the property. /// The property. /// The value. - T GetValue(DirectPropertyBase property); - - /// - /// Gets an base value. - /// - /// The type of the property. - /// The property. - /// The maximum priority for the value. - /// - /// Gets the value of the property, if set on this object with a priority equal or lower to - /// , otherwise . Note that - /// this method does not return property values that come from inherited or default values. - /// - Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority); + object? GetValue(AvaloniaProperty property); /// /// Checks whether a is animating. @@ -71,93 +43,35 @@ namespace Avalonia /// /// Sets a value. /// - /// The type of the property. /// The property. /// The value. /// The priority of the value. /// /// An if setting the property can be undone, otherwise null. /// - IDisposable? SetValue( - StyledPropertyBase property, - T value, + IDisposable? SetValue( + AvaloniaProperty property, + object? value, BindingPriority priority = BindingPriority.LocalValue); - /// - /// Sets a value. - /// - /// The type of the property. - /// The property. - /// The value. - void SetValue(DirectPropertyBase property, T value); - /// /// Binds a to an observable. /// - /// The type of the property. /// The property. /// The observable. /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// - IDisposable Bind( - StyledPropertyBase property, - IObservable> source, + IDisposable Bind( + AvaloniaProperty property, + IObservable source, BindingPriority priority = BindingPriority.LocalValue); - /// - /// Binds a to an observable. - /// - /// The type of the property. - /// The property. - /// The observable. - /// - /// A disposable which can be used to terminate the binding. - /// - IDisposable Bind( - DirectPropertyBase property, - IObservable> source); - /// /// Coerces the specified . /// - /// The type of the property. /// The property. - void CoerceValue(StyledPropertyBase property); - - /// - /// Registers an object as an inheritance child. - /// - /// The inheritance child. - /// - /// Inheritance children will receive a call to - /// - /// when an inheritable property value changes on the parent. - /// - void AddInheritanceChild(IAvaloniaObject child); - - /// - /// Unregisters an object as an inheritance child. - /// - /// The inheritance child. - /// - /// Removes an inheritance child that was added by a call to - /// . - /// - void RemoveInheritanceChild(IAvaloniaObject child); - - /// - /// Called when an inheritable property changes on an object registered as an inheritance - /// parent. - /// - /// The type of the value. - /// The property that has changed. - /// - /// - void InheritedPropertyChanged( - AvaloniaProperty property, - Optional oldValue, - Optional newValue); + void CoerceValue(AvaloniaProperty property); } } diff --git a/src/Avalonia.Styling/IDataContextProvider.cs b/src/Avalonia.Base/IDataContextProvider.cs similarity index 100% rename from src/Avalonia.Styling/IDataContextProvider.cs rename to src/Avalonia.Base/IDataContextProvider.cs diff --git a/src/Avalonia.Styling/INamed.cs b/src/Avalonia.Base/INamed.cs similarity index 100% rename from src/Avalonia.Styling/INamed.cs rename to src/Avalonia.Base/INamed.cs diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Base/IStyledElement.cs similarity index 100% rename from src/Avalonia.Styling/IStyledElement.cs rename to src/Avalonia.Base/IStyledElement.cs diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs similarity index 100% rename from src/Avalonia.Input/AccessKeyHandler.cs rename to src/Avalonia.Base/Input/AccessKeyHandler.cs diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Base/Input/Cursor.cs similarity index 100% rename from src/Avalonia.Input/Cursor.cs rename to src/Avalonia.Base/Input/Cursor.cs diff --git a/src/Avalonia.Input/DataFormats.cs b/src/Avalonia.Base/Input/DataFormats.cs similarity index 100% rename from src/Avalonia.Input/DataFormats.cs rename to src/Avalonia.Base/Input/DataFormats.cs diff --git a/src/Avalonia.Input/DataObject.cs b/src/Avalonia.Base/Input/DataObject.cs similarity index 100% rename from src/Avalonia.Input/DataObject.cs rename to src/Avalonia.Base/Input/DataObject.cs diff --git a/src/Avalonia.Input/DragDrop.cs b/src/Avalonia.Base/Input/DragDrop.cs similarity index 100% rename from src/Avalonia.Input/DragDrop.cs rename to src/Avalonia.Base/Input/DragDrop.cs diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Base/Input/DragDropDevice.cs similarity index 100% rename from src/Avalonia.Input/DragDropDevice.cs rename to src/Avalonia.Base/Input/DragDropDevice.cs diff --git a/src/Avalonia.Input/DragDropEffects.cs b/src/Avalonia.Base/Input/DragDropEffects.cs similarity index 100% rename from src/Avalonia.Input/DragDropEffects.cs rename to src/Avalonia.Base/Input/DragDropEffects.cs diff --git a/src/Avalonia.Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs similarity index 100% rename from src/Avalonia.Input/DragEventArgs.cs rename to src/Avalonia.Base/Input/DragEventArgs.cs diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs similarity index 100% rename from src/Avalonia.Input/FocusManager.cs rename to src/Avalonia.Base/Input/FocusManager.cs diff --git a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs similarity index 100% rename from src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs rename to src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs diff --git a/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/IGestureRecognizer.cs similarity index 100% rename from src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs rename to src/Avalonia.Base/Input/GestureRecognizers/IGestureRecognizer.cs diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs similarity index 100% rename from src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs rename to src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs new file mode 100644 index 0000000000..4349fca7db --- /dev/null +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -0,0 +1,142 @@ +using System; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public static class Gestures + { + private static bool s_isDoubleTapped = false; + public static readonly RoutedEvent TappedEvent = RoutedEvent.Register( + "Tapped", + RoutingStrategies.Bubble, + typeof(Gestures)); + + public static readonly RoutedEvent DoubleTappedEvent = RoutedEvent.Register( + "DoubleTapped", + RoutingStrategies.Bubble, + typeof(Gestures)); + + public static readonly RoutedEvent RightTappedEvent = RoutedEvent.Register( + "RightTapped", + RoutingStrategies.Bubble, + typeof(Gestures)); + + public static readonly RoutedEvent ScrollGestureEvent = + RoutedEvent.Register( + "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent ScrollGestureEndedEvent = + RoutedEvent.Register( + "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = + RoutedEvent.Register( + "PointerMagnifyGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = + RoutedEvent.Register( + "PointerRotateGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = + RoutedEvent.Register( + "PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures)); + +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + private static readonly WeakReference s_lastPress = new WeakReference(null); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + + static Gestures() + { + InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); + InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); + } + + public static void AddTappedHandler(IInteractive element, EventHandler handler) + { + element.AddHandler(TappedEvent, handler); + } + + public static void AddDoubleTappedHandler(IInteractive element, EventHandler handler) + { + element.AddHandler(DoubleTappedEvent, handler); + } + + public static void AddRightTappedHandler(IInteractive element, EventHandler handler) + { + element.AddHandler(RightTappedEvent, handler); + } + + public static void RemoveTappedHandler(IInteractive element, EventHandler handler) + { + element.RemoveHandler(TappedEvent, handler); + } + + public static void RemoveDoubleTappedHandler(IInteractive element, EventHandler handler) + { + element.RemoveHandler(DoubleTappedEvent, handler); + } + + public static void RemoveRightTappedHandler(IInteractive element, EventHandler handler) + { + element.RemoveHandler(RightTappedEvent, handler); + } + + private static void PointerPressed(RoutedEventArgs ev) + { + if (ev.Source is null) + { + return; + } + + if (ev.Route == RoutingStrategies.Bubble) + { + var e = (PointerPressedEventArgs)ev; + var visual = (IVisual)ev.Source; + + if (e.ClickCount <= 1) + { + s_isDoubleTapped = false; + s_lastPress.SetTarget(ev.Source); + } + else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) + { + if (s_lastPress.TryGetTarget(out var target) && target == e.Source) + { + s_isDoubleTapped = true; + e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); + } + } + else + { + s_isDoubleTapped = false; + } + } + } + + private static void PointerReleased(RoutedEventArgs ev) + { + if (ev.Route == RoutingStrategies.Bubble) + { + var e = (PointerReleasedEventArgs)ev; + + if (s_lastPress.TryGetTarget(out var target) && target == e.Source) + { + if (e.InitialPressMouseButton == MouseButton.Left || e.InitialPressMouseButton == MouseButton.Right) + { + if (e.InitialPressMouseButton == MouseButton.Right) + { + e.Source.RaiseEvent(new TappedEventArgs(RightTappedEvent, e)); + } + //s_isDoubleTapped needed here to prevent invoking Tapped event when DoubleTapped is called. + //This behaviour matches UWP behaviour. + else if (s_isDoubleTapped == false) + { + e.Source.RaiseEvent(new TappedEventArgs(TappedEvent, e)); + } + } + } + } + } + } +} diff --git a/src/Avalonia.Input/GotFocusEventArgs.cs b/src/Avalonia.Base/Input/GotFocusEventArgs.cs similarity index 100% rename from src/Avalonia.Input/GotFocusEventArgs.cs rename to src/Avalonia.Base/Input/GotFocusEventArgs.cs diff --git a/src/Avalonia.Input/IAccessKeyHandler.cs b/src/Avalonia.Base/Input/IAccessKeyHandler.cs similarity index 100% rename from src/Avalonia.Input/IAccessKeyHandler.cs rename to src/Avalonia.Base/Input/IAccessKeyHandler.cs diff --git a/src/Avalonia.Base/Input/IClickableControl.cs b/src/Avalonia.Base/Input/IClickableControl.cs new file mode 100644 index 0000000000..d6d887520b --- /dev/null +++ b/src/Avalonia.Base/Input/IClickableControl.cs @@ -0,0 +1,18 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + /// + /// + /// + internal interface IClickableControl + { + event EventHandler Click; + void RaiseClick(); + /// + /// Gets a value indicating whether this control and all its parents are enabled. + /// + bool IsEffectivelyEnabled { get; } + } +} diff --git a/src/Avalonia.Input/ICloseable.cs b/src/Avalonia.Base/Input/ICloseable.cs similarity index 100% rename from src/Avalonia.Input/ICloseable.cs rename to src/Avalonia.Base/Input/ICloseable.cs diff --git a/src/Avalonia.Input/ICommandSource.cs b/src/Avalonia.Base/Input/ICommandSource.cs similarity index 100% rename from src/Avalonia.Input/ICommandSource.cs rename to src/Avalonia.Base/Input/ICommandSource.cs diff --git a/src/Avalonia.Input/ICustomKeyboardNavigation.cs b/src/Avalonia.Base/Input/ICustomKeyboardNavigation.cs similarity index 100% rename from src/Avalonia.Input/ICustomKeyboardNavigation.cs rename to src/Avalonia.Base/Input/ICustomKeyboardNavigation.cs diff --git a/src/Avalonia.Input/IDataObject.cs b/src/Avalonia.Base/Input/IDataObject.cs similarity index 100% rename from src/Avalonia.Input/IDataObject.cs rename to src/Avalonia.Base/Input/IDataObject.cs diff --git a/src/Avalonia.Input/IFocusManager.cs b/src/Avalonia.Base/Input/IFocusManager.cs similarity index 100% rename from src/Avalonia.Input/IFocusManager.cs rename to src/Avalonia.Base/Input/IFocusManager.cs diff --git a/src/Avalonia.Input/IFocusScope.cs b/src/Avalonia.Base/Input/IFocusScope.cs similarity index 100% rename from src/Avalonia.Input/IFocusScope.cs rename to src/Avalonia.Base/Input/IFocusScope.cs diff --git a/src/Avalonia.Input/IInputDevice.cs b/src/Avalonia.Base/Input/IInputDevice.cs similarity index 100% rename from src/Avalonia.Input/IInputDevice.cs rename to src/Avalonia.Base/Input/IInputDevice.cs diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Base/Input/IInputElement.cs similarity index 100% rename from src/Avalonia.Input/IInputElement.cs rename to src/Avalonia.Base/Input/IInputElement.cs diff --git a/src/Avalonia.Input/IInputManager.cs b/src/Avalonia.Base/Input/IInputManager.cs similarity index 100% rename from src/Avalonia.Input/IInputManager.cs rename to src/Avalonia.Base/Input/IInputManager.cs diff --git a/src/Avalonia.Base/Input/IInputRoot.cs b/src/Avalonia.Base/Input/IInputRoot.cs new file mode 100644 index 0000000000..98e8699573 --- /dev/null +++ b/src/Avalonia.Base/Input/IInputRoot.cs @@ -0,0 +1,33 @@ +namespace Avalonia.Input +{ + /// + /// Defines the interface for top-level input elements. + /// + public interface IInputRoot : IInputElement + { + /// + /// Gets or sets the access key handler. + /// + IAccessKeyHandler AccessKeyHandler { get; } + + /// + /// Gets or sets the keyboard navigation handler. + /// + IKeyboardNavigationHandler KeyboardNavigationHandler { get; } + + /// + /// Gets or sets the input element that the pointer is currently over. + /// + IInputElement? PointerOverElement { get; set; } + + /// + /// Gets or sets a value indicating whether access keys are shown in the window. + /// + bool ShowAccessKeys { get; set; } + + /// + /// Gets associated mouse device + /// + IMouseDevice? MouseDevice { get; } + } +} diff --git a/src/Avalonia.Base/Input/IKeyboardDevice.cs b/src/Avalonia.Base/Input/IKeyboardDevice.cs new file mode 100644 index 0000000000..d0e84e5ad0 --- /dev/null +++ b/src/Avalonia.Base/Input/IKeyboardDevice.cs @@ -0,0 +1,62 @@ +using System; +using System.ComponentModel; + +namespace Avalonia.Input +{ + [Flags, Obsolete("Use KeyModifiers and PointerPointProperties")] + public enum InputModifiers + { + None = 0, + Alt = 1, + Control = 2, + Shift = 4, + Windows = 8, + LeftMouseButton = 16, + RightMouseButton = 32, + MiddleMouseButton = 64 + } + + [Flags] + public enum KeyModifiers + { + None = 0, + Alt = 1, + Control = 2, + Shift = 4, + Meta = 8, + } + + [Flags] + public enum KeyStates + { + None = 0, + Down = 1, + Toggled = 2, + } + + [Flags] + public enum RawInputModifiers + { + None = 0, + Alt = 1, + Control = 2, + Shift = 4, + Meta = 8, + LeftMouseButton = 16, + RightMouseButton = 32, + MiddleMouseButton = 64, + XButton1MouseButton = 128, + XButton2MouseButton = 256, + KeyboardMask = Alt | Control | Shift | Meta + } + + public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged + { + IInputElement? FocusedElement { get; } + + void SetFocusedElement( + IInputElement? element, + NavigationMethod method, + KeyModifiers modifiers); + } +} diff --git a/src/Avalonia.Input/IKeyboardNavigationHandler.cs b/src/Avalonia.Base/Input/IKeyboardNavigationHandler.cs similarity index 100% rename from src/Avalonia.Input/IKeyboardNavigationHandler.cs rename to src/Avalonia.Base/Input/IKeyboardNavigationHandler.cs diff --git a/src/Avalonia.Input/IMainMenu.cs b/src/Avalonia.Base/Input/IMainMenu.cs similarity index 100% rename from src/Avalonia.Input/IMainMenu.cs rename to src/Avalonia.Base/Input/IMainMenu.cs diff --git a/src/Avalonia.Base/Input/IMouseDevice.cs b/src/Avalonia.Base/Input/IMouseDevice.cs new file mode 100644 index 0000000000..6b7f0e76e5 --- /dev/null +++ b/src/Avalonia.Base/Input/IMouseDevice.cs @@ -0,0 +1,22 @@ +using System; + +namespace Avalonia.Input +{ + /// + /// Represents a mouse device. + /// + public interface IMouseDevice : IPointerDevice + { + /// + /// Gets the mouse position, in screen coordinates. + /// + [Obsolete("Use PointerEventArgs.GetPosition")] + PixelPoint Position { get; } + + [Obsolete] + void TopLevelClosed(IInputRoot root); + + [Obsolete] + void SceneInvalidated(IInputRoot root, Rect rect); + } +} diff --git a/src/Avalonia.Input/INavigableContainer.cs b/src/Avalonia.Base/Input/INavigableContainer.cs similarity index 100% rename from src/Avalonia.Input/INavigableContainer.cs rename to src/Avalonia.Base/Input/INavigableContainer.cs diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Base/Input/IPointer.cs similarity index 100% rename from src/Avalonia.Input/IPointer.cs rename to src/Avalonia.Base/Input/IPointer.cs diff --git a/src/Avalonia.Base/Input/IPointerDevice.cs b/src/Avalonia.Base/Input/IPointerDevice.cs new file mode 100644 index 0000000000..0096bb77bf --- /dev/null +++ b/src/Avalonia.Base/Input/IPointerDevice.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.VisualTree; +using Avalonia.Input.Raw; + +namespace Avalonia.Input +{ + public interface IPointerDevice : IInputDevice + { + /// + [Obsolete("Use IPointer")] + IInputElement? Captured { get; } + + /// + [Obsolete("Use IPointer")] + void Capture(IInputElement? control); + + /// + [Obsolete("Use PointerEventArgs.GetPosition")] + Point GetPosition(IVisual relativeTo); + + /// + /// Gets a pointer for specific event args. + /// + /// + /// If pointer doesn't exist or wasn't yet created this method will return null. + /// + /// Raw pointer event args associated with the pointer. + /// The pointer. + IPointer? TryGetPointer(RawPointerEventArgs ev); + } +} diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs new file mode 100644 index 0000000000..9fe07f62dd --- /dev/null +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -0,0 +1,693 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Data; +using Avalonia.Input.GestureRecognizers; +using Avalonia.Input.TextInput; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +#nullable enable + +namespace Avalonia.Input +{ + /// + /// Implements input-related functionality for a control. + /// + [PseudoClasses(":disabled", ":focus", ":focus-visible", ":focus-within", ":pointerover")] + public class InputElement : Interactive, IInputElement + { + /// + /// Defines the property. + /// + public static readonly StyledProperty FocusableProperty = + AvaloniaProperty.Register(nameof(Focusable)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsEnabledProperty = + AvaloniaProperty.Register(nameof(IsEnabled), true); + + /// + /// Defines the property. + /// + public static readonly DirectProperty IsEffectivelyEnabledProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsEffectivelyEnabled), + o => o.IsEffectivelyEnabled); + + /// + /// Gets or sets associated mouse cursor. + /// + public static readonly StyledProperty CursorProperty = + AvaloniaProperty.Register(nameof(Cursor), null, true); + + /// + /// Defines the property. + /// + public static readonly DirectProperty IsKeyboardFocusWithinProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsKeyboardFocusWithin), + o => o.IsKeyboardFocusWithin); + + /// + /// Defines the property. + /// + public static readonly DirectProperty IsFocusedProperty = + AvaloniaProperty.RegisterDirect(nameof(IsFocused), o => o.IsFocused); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsHitTestVisibleProperty = + AvaloniaProperty.Register(nameof(IsHitTestVisible), true); + + /// + /// Defines the property. + /// + public static readonly DirectProperty IsPointerOverProperty = + AvaloniaProperty.RegisterDirect(nameof(IsPointerOver), o => o.IsPointerOver); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsTabStopProperty = + KeyboardNavigation.IsTabStopProperty.AddOwner(); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent GotFocusEvent = + RoutedEvent.Register(nameof(GotFocus), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent LostFocusEvent = + RoutedEvent.Register(nameof(LostFocus), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent KeyDownEvent = + RoutedEvent.Register( + "KeyDown", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent KeyUpEvent = + RoutedEvent.Register( + "KeyUp", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the property. + /// + public static readonly StyledProperty TabIndexProperty = + KeyboardNavigation.TabIndexProperty.AddOwner(); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TextInputEvent = + RoutedEvent.Register( + "TextInput", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TextInputMethodClientRequestedEvent = + RoutedEvent.Register( + "TextInputMethodClientRequested", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerEnterEvent = + RoutedEvent.Register(nameof(PointerEnter), RoutingStrategies.Direct); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerLeaveEvent = + RoutedEvent.Register(nameof(PointerLeave), RoutingStrategies.Direct); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerMovedEvent = + RoutedEvent.Register( + "PointerMove", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerPressedEvent = + RoutedEvent.Register( + "PointerPressed", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerReleasedEvent = + RoutedEvent.Register( + "PointerReleased", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent PointerCaptureLostEvent = + RoutedEvent.Register( + "PointerCaptureLost", + RoutingStrategies.Direct); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerWheelChangedEvent = + RoutedEvent.Register( + "PointerWheelChanged", + RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; + + /// + /// Defines the event. + /// + public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; + + private bool _isEffectivelyEnabled = true; + private bool _isFocused; + private bool _isKeyboardFocusWithin; + private bool _isFocusVisible; + private bool _isPointerOver; + private GestureRecognizerCollection? _gestureRecognizers; + + /// + /// Initializes static members of the class. + /// + static InputElement() + { + IsEnabledProperty.Changed.Subscribe(IsEnabledChanged); + + GotFocusEvent.AddClassHandler((x, e) => x.OnGotFocus(e)); + LostFocusEvent.AddClassHandler((x, e) => x.OnLostFocus(e)); + KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e)); + KeyUpEvent.AddClassHandler((x, e) => x.OnKeyUp(e)); + TextInputEvent.AddClassHandler((x, e) => x.OnTextInput(e)); + PointerEnterEvent.AddClassHandler((x, e) => x.OnPointerEnterCore(e)); + PointerLeaveEvent.AddClassHandler((x, e) => x.OnPointerLeaveCore(e)); + PointerMovedEvent.AddClassHandler((x, e) => x.OnPointerMoved(e)); + PointerPressedEvent.AddClassHandler((x, e) => x.OnPointerPressed(e)); + PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); + PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); + PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); + } + + public InputElement() + { + UpdatePseudoClasses(IsFocused, IsPointerOver); + } + + /// + /// Occurs when the control receives focus. + /// + public event EventHandler? GotFocus + { + add { AddHandler(GotFocusEvent, value); } + remove { RemoveHandler(GotFocusEvent, value); } + } + + /// + /// Occurs when the control loses focus. + /// + public event EventHandler? LostFocus + { + add { AddHandler(LostFocusEvent, value); } + remove { RemoveHandler(LostFocusEvent, value); } + } + + /// + /// Occurs when a key is pressed while the control has focus. + /// + public event EventHandler? KeyDown + { + add { AddHandler(KeyDownEvent, value); } + remove { RemoveHandler(KeyDownEvent, value); } + } + + /// + /// Occurs when a key is released while the control has focus. + /// + public event EventHandler? KeyUp + { + add { AddHandler(KeyUpEvent, value); } + remove { RemoveHandler(KeyUpEvent, value); } + } + + /// + /// Occurs when a user typed some text while the control has focus. + /// + public event EventHandler? TextInput + { + add { AddHandler(TextInputEvent, value); } + remove { RemoveHandler(TextInputEvent, value); } + } + + /// + /// Occurs when an input element gains input focus and input method is looking for the corresponding client + /// + public event EventHandler? TextInputMethodClientRequested + { + add { AddHandler(TextInputMethodClientRequestedEvent, value); } + remove { RemoveHandler(TextInputMethodClientRequestedEvent, value); } + } + + /// + /// Occurs when the pointer enters the control. + /// + public event EventHandler? PointerEnter + { + add { AddHandler(PointerEnterEvent, value); } + remove { RemoveHandler(PointerEnterEvent, value); } + } + + /// + /// Occurs when the pointer leaves the control. + /// + public event EventHandler? PointerLeave + { + add { AddHandler(PointerLeaveEvent, value); } + remove { RemoveHandler(PointerLeaveEvent, value); } + } + + /// + /// Occurs when the pointer moves over the control. + /// + public event EventHandler? PointerMoved + { + add { AddHandler(PointerMovedEvent, value); } + remove { RemoveHandler(PointerMovedEvent, value); } + } + + /// + /// Occurs when the pointer is pressed over the control. + /// + public event EventHandler? PointerPressed + { + add { AddHandler(PointerPressedEvent, value); } + remove { RemoveHandler(PointerPressedEvent, value); } + } + + /// + /// Occurs when the pointer is released over the control. + /// + public event EventHandler? PointerReleased + { + add { AddHandler(PointerReleasedEvent, value); } + remove { RemoveHandler(PointerReleasedEvent, value); } + } + + /// + /// Occurs when the control or its child control loses the pointer capture for any reason, + /// event will not be triggered for a parent control if capture was transferred to another child of that parent control + /// + public event EventHandler? PointerCaptureLost + { + add => AddHandler(PointerCaptureLostEvent, value); + remove => RemoveHandler(PointerCaptureLostEvent, value); + } + + /// + /// Occurs when the mouse is scrolled over the control. + /// + public event EventHandler? PointerWheelChanged + { + add { AddHandler(PointerWheelChangedEvent, value); } + remove { RemoveHandler(PointerWheelChangedEvent, value); } + } + + /// + /// Occurs when a tap gesture occurs on the control. + /// + public event EventHandler? Tapped + { + add { AddHandler(TappedEvent, value); } + remove { RemoveHandler(TappedEvent, value); } + } + + /// + /// Occurs when a double-tap gesture occurs on the control. + /// + public event EventHandler? DoubleTapped + { + add { AddHandler(DoubleTappedEvent, value); } + remove { RemoveHandler(DoubleTappedEvent, value); } + } + + /// + /// Gets or sets a value indicating whether the control can receive focus. + /// + public bool Focusable + { + get { return GetValue(FocusableProperty); } + set { SetValue(FocusableProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the control is enabled for user interaction. + /// + public bool IsEnabled + { + get { return GetValue(IsEnabledProperty); } + set { SetValue(IsEnabledProperty, value); } + } + + /// + /// Gets or sets associated mouse cursor. + /// + public Cursor? Cursor + { + get { return GetValue(CursorProperty); } + set { SetValue(CursorProperty, value); } + } + + /// + /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements. + /// + public bool IsKeyboardFocusWithin + { + get => _isKeyboardFocusWithin; + internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value); + } + + /// + /// Gets a value indicating whether the control is focused. + /// + public bool IsFocused + { + get { return _isFocused; } + private set { SetAndRaise(IsFocusedProperty, ref _isFocused, value); } + } + + /// + /// Gets or sets a value indicating whether the control is considered for hit testing. + /// + public bool IsHitTestVisible + { + get { return GetValue(IsHitTestVisibleProperty); } + set { SetValue(IsHitTestVisibleProperty, value); } + } + + /// + /// Gets a value indicating whether the pointer is currently over the control. + /// + public bool IsPointerOver + { + get { return _isPointerOver; } + internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } + } + + /// + /// Gets or sets a value that indicates whether the control is included in tab navigation. + /// + public bool IsTabStop + { + get => GetValue(IsTabStopProperty); + set => SetValue(IsTabStopProperty, value); + } + + /// + public bool IsEffectivelyEnabled + { + get => _isEffectivelyEnabled; + private set + { + SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); + PseudoClasses.Set(":disabled", !value); + } + } + + /// + /// Gets or sets a value that determines the order in which elements receive focus when the + /// user navigates through controls by pressing the Tab key. + /// + public int TabIndex + { + get => GetValue(TabIndexProperty); + set => SetValue(TabIndexProperty, value); + } + + public List KeyBindings { get; } = new List(); + + /// + /// Allows a derived class to override the enabled state of the control. + /// + /// + /// Derived controls may wish to disable the enabled state of the control without overwriting the + /// user-supplied setting. This can be done by overriding this property + /// to return the overridden enabled state. If the value returned from + /// should change, then the derived control should call . + /// + protected virtual bool IsEnabledCore => IsEnabled; + + public GestureRecognizerCollection GestureRecognizers + => _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this)); + + /// + /// Focuses the control. + /// + public void Focus() + { + FocusManager.Instance?.Focus(this); + } + + /// + protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTreeCore(e); + + if (IsFocused) + { + FocusManager.Instance?.Focus(null); + } + } + + /// + protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTreeCore(e); + UpdateIsEffectivelyEnabled(); + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnGotFocus(GotFocusEventArgs e) + { + var isFocused = e.Source == this; + _isFocusVisible = isFocused && (e.NavigationMethod == NavigationMethod.Directional || e.NavigationMethod == NavigationMethod.Tab); + IsFocused = isFocused; + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnLostFocus(RoutedEventArgs e) + { + _isFocusVisible = false; + IsFocused = false; + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnKeyDown(KeyEventArgs e) + { + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnKeyUp(KeyEventArgs e) + { + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnTextInput(TextInputEventArgs e) + { + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnPointerEnter(PointerEventArgs e) + { + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnPointerLeave(PointerEventArgs e) + { + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnPointerMoved(PointerEventArgs e) + { + if (_gestureRecognizers?.HandlePointerMoved(e) == true) + e.Handled = true; + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnPointerPressed(PointerPressedEventArgs e) + { + if (_gestureRecognizers?.HandlePointerPressed(e) == true) + e.Handled = true; + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnPointerReleased(PointerReleasedEventArgs e) + { + if (_gestureRecognizers?.HandlePointerReleased(e) == true) + e.Handled = true; + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _gestureRecognizers?.HandlePointerCaptureLost(e); + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnPointerWheelChanged(PointerWheelEventArgs e) + { + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsFocusedProperty) + { + UpdatePseudoClasses(change.GetNewValue(), null); + } + else if (change.Property == IsPointerOverProperty) + { + UpdatePseudoClasses(null, change.GetNewValue()); + } + else if (change.Property == IsKeyboardFocusWithinProperty) + { + PseudoClasses.Set(":focus-within", change.GetNewValue()); + } + } + + /// + /// Updates the property value according to the parent + /// control's enabled state and . + /// + protected void UpdateIsEffectivelyEnabled() + { + UpdateIsEffectivelyEnabled(this.GetVisualParent()); + } + + private static void IsEnabledChanged(AvaloniaPropertyChangedEventArgs e) + { + ((InputElement)e.Sender).UpdateIsEffectivelyEnabled(); + } + + /// + /// Called before the event occurs. + /// + /// The event args. + private void OnPointerEnterCore(PointerEventArgs e) + { + IsPointerOver = true; + OnPointerEnter(e); + } + + /// + /// Called before the event occurs. + /// + /// The event args. + private void OnPointerLeaveCore(PointerEventArgs e) + { + IsPointerOver = false; + OnPointerLeave(e); + } + + /// + /// Updates the property based on the parent's + /// . + /// + /// The parent control. + private void UpdateIsEffectivelyEnabled(InputElement? parent) + { + IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true); + + // PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ + // will cause extra allocations and overhead. + + var children = VisualChildren; + + // ReSharper disable once ForCanBeConvertedToForeach + for (int i = 0; i < children.Count; ++i) + { + var child = children[i] as InputElement; + + child?.UpdateIsEffectivelyEnabled(this); + } + } + + private void UpdatePseudoClasses(bool? isFocused, bool? isPointerOver) + { + if (isFocused.HasValue) + { + PseudoClasses.Set(":focus", isFocused.Value); + PseudoClasses.Set(":focus-visible", _isFocusVisible); + } + + if (isPointerOver.HasValue) + { + PseudoClasses.Set(":pointerover", isPointerOver.Value); + } + } + } +} diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Base/Input/InputExtensions.cs similarity index 100% rename from src/Avalonia.Input/InputExtensions.cs rename to src/Avalonia.Base/Input/InputExtensions.cs diff --git a/src/Avalonia.Input/InputManager.cs b/src/Avalonia.Base/Input/InputManager.cs similarity index 100% rename from src/Avalonia.Input/InputManager.cs rename to src/Avalonia.Base/Input/InputManager.cs diff --git a/src/Avalonia.Input/InputMethod.cs b/src/Avalonia.Base/Input/InputMethod.cs similarity index 100% rename from src/Avalonia.Input/InputMethod.cs rename to src/Avalonia.Base/Input/InputMethod.cs diff --git a/src/Avalonia.Input/Key.cs b/src/Avalonia.Base/Input/Key.cs similarity index 100% rename from src/Avalonia.Input/Key.cs rename to src/Avalonia.Base/Input/Key.cs diff --git a/src/Avalonia.Input/KeyBinding.cs b/src/Avalonia.Base/Input/KeyBinding.cs similarity index 100% rename from src/Avalonia.Input/KeyBinding.cs rename to src/Avalonia.Base/Input/KeyBinding.cs diff --git a/src/Avalonia.Input/KeyEventArgs.cs b/src/Avalonia.Base/Input/KeyEventArgs.cs similarity index 100% rename from src/Avalonia.Input/KeyEventArgs.cs rename to src/Avalonia.Base/Input/KeyEventArgs.cs diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs new file mode 100644 index 0000000000..3b7a828b86 --- /dev/null +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Utilities; + +namespace Avalonia.Input +{ + /// + /// Defines a keyboard input combination. + /// + public sealed class KeyGesture : IEquatable + { + private static readonly Dictionary s_keySynonyms = new Dictionary + { + { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod }, { ",", Key.OemComma } + }; + + [Obsolete("Use constructor taking KeyModifiers")] + public KeyGesture(Key key, InputModifiers modifiers) + { + Key = key; + KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xf); + } + + public KeyGesture(Key key, KeyModifiers modifiers = KeyModifiers.None) + { + Key = key; + KeyModifiers = modifiers; + } + + public bool Equals(KeyGesture? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Key == other.Key && KeyModifiers == other.KeyModifiers; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + + return obj is KeyGesture gesture && Equals(gesture); + } + + public override int GetHashCode() + { + unchecked + { + return ((int)Key * 397) ^ (int)KeyModifiers; + } + } + + public static bool operator ==(KeyGesture? left, KeyGesture? right) + { + return Equals(left, right); + } + + public static bool operator !=(KeyGesture? left, KeyGesture? right) + { + return !Equals(left, right); + } + + public Key Key { get; } + + [Obsolete("Use KeyModifiers")] + public InputModifiers Modifiers => (InputModifiers)KeyModifiers; + + public KeyModifiers KeyModifiers { get; } + + public static KeyGesture Parse(string gesture) + { + // string.Split can't be used here because "Ctrl++" is a perfectly valid key gesture + + var key = Key.None; + var keyModifiers = KeyModifiers.None; + + var cstart = 0; + + for (var c = 0; c <= gesture.Length; c++) + { + var ch = c == gesture.Length ? '\0' : gesture[c]; + bool isLast = c == gesture.Length; + + if (isLast || (ch == '+' && cstart != c)) + { + var partSpan = gesture.AsSpan(cstart, c - cstart).Trim(); + + if (isLast) + { + key = ParseKey(partSpan.ToString()); + } + else + { + keyModifiers |= ParseModifier(partSpan); + } + + cstart = c + 1; + } + } + + + return new KeyGesture(key, keyModifiers); + } + + public override string ToString() + { + var s = new StringBuilder(); + + static void Plus(StringBuilder s) + { + if (s.Length > 0) + { + s.Append("+"); + } + } + + if (KeyModifiers.HasAllFlags(KeyModifiers.Control)) + { + s.Append("Ctrl"); + } + + if (KeyModifiers.HasAllFlags(KeyModifiers.Shift)) + { + Plus(s); + s.Append("Shift"); + } + + if (KeyModifiers.HasAllFlags(KeyModifiers.Alt)) + { + Plus(s); + s.Append("Alt"); + } + + if (KeyModifiers.HasAllFlags(KeyModifiers.Meta)) + { + Plus(s); + s.Append("Cmd"); + } + + Plus(s); + s.Append(Key); + + return s.ToString(); + } + + public bool Matches(KeyEventArgs keyEvent) => + keyEvent != null && + keyEvent.KeyModifiers == KeyModifiers && + ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key); + + // TODO: Move that to external key parser + private static Key ParseKey(string key) + { + if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv)) + return rv; + + return EnumHelper.Parse(key, true); + } + + private static KeyModifiers ParseModifier(ReadOnlySpan modifier) + { + if (modifier.Equals("ctrl".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return KeyModifiers.Control; + } + + if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase) || + modifier.Equals("win".AsSpan(), StringComparison.OrdinalIgnoreCase) || + modifier.Equals("⌘".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return KeyModifiers.Meta; + } + + return EnumHelper.Parse(modifier.ToString(), true); + } + + private Key ResolveNumPadOperationKey(Key key) + { + switch (key) + { + case Key.Add: + return Key.OemPlus; + case Key.Subtract: + return Key.OemMinus; + case Key.Decimal: + return Key.OemPeriod; + default: + return key; + } + } + } +} diff --git a/src/Avalonia.Base/Input/KeyboardDevice.cs b/src/Avalonia.Base/Input/KeyboardDevice.cs new file mode 100644 index 0000000000..0600b54618 --- /dev/null +++ b/src/Avalonia.Base/Input/KeyboardDevice.cs @@ -0,0 +1,249 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged + { + private IInputElement? _focusedElement; + private IInputRoot? _focusedRoot; + + public event PropertyChangedEventHandler? PropertyChanged; + + public static IKeyboardDevice? Instance => AvaloniaLocator.Current.GetService(); + + public IInputManager? InputManager => AvaloniaLocator.Current.GetService(); + + public IFocusManager? FocusManager => AvaloniaLocator.Current.GetService(); + + // This should live in the FocusManager, but with the current outdated architecture + // the source of truth about the input focus is in KeyboardDevice + private readonly TextInputMethodManager _textInputManager = new TextInputMethodManager(); + + public IInputElement? FocusedElement => _focusedElement; + + private void ClearFocusWithinAncestors(IInputElement? element) + { + var el = element; + + while (el != null) + { + if (el is InputElement ie) + { + ie.IsKeyboardFocusWithin = false; + } + + el = (IInputElement?)el.VisualParent; + } + } + + private void ClearFocusWithin(IInputElement element, bool clearRoot) + { + foreach (var visual in element.VisualChildren) + { + if (visual is IInputElement el && el.IsKeyboardFocusWithin) + { + ClearFocusWithin(el, true); + break; + } + } + + if (clearRoot) + { + if (element is InputElement ie) + { + ie.IsKeyboardFocusWithin = false; + } + } + } + + private void SetIsFocusWithin(IInputElement? oldElement, IInputElement? newElement) + { + if (newElement == null && oldElement != null) + { + ClearFocusWithinAncestors(oldElement); + return; + } + + IInputElement? branch = null; + + var el = newElement; + + while (el != null) + { + if (el.IsKeyboardFocusWithin) + { + branch = el; + break; + } + + el = el.VisualParent as IInputElement; + } + + el = oldElement; + + if (el != null && branch != null) + { + ClearFocusWithin(branch, false); + } + + el = newElement; + + while (el != null && el != branch) + { + if (el is InputElement ie) + { + ie.IsKeyboardFocusWithin = true; + } + + el = el.VisualParent as IInputElement; + } + } + + private void ClearChildrenFocusWithin(IInputElement element, bool clearRoot) + { + foreach (var visual in element.VisualChildren) + { + if (visual is IInputElement el && el.IsKeyboardFocusWithin) + { + ClearChildrenFocusWithin(el, true); + break; + } + } + + if (clearRoot && element is InputElement ie) + { + ie.IsKeyboardFocusWithin = false; + } + } + + public void SetFocusedElement( + IInputElement? element, + NavigationMethod method, + KeyModifiers keyModifiers) + { + if (element != FocusedElement) + { + var interactive = FocusedElement as IInteractive; + + if (FocusedElement != null && + (!FocusedElement.IsAttachedToVisualTree || + _focusedRoot != element?.VisualRoot as IInputRoot) && + _focusedRoot != null) + { + ClearChildrenFocusWithin(_focusedRoot, true); + } + + SetIsFocusWithin(FocusedElement, element); + _focusedElement = element; + _focusedRoot = _focusedElement?.VisualRoot as IInputRoot; + + interactive?.RaiseEvent(new RoutedEventArgs + { + RoutedEvent = InputElement.LostFocusEvent, + }); + + interactive = element as IInteractive; + + interactive?.RaiseEvent(new GotFocusEventArgs + { + RoutedEvent = InputElement.GotFocusEvent, + NavigationMethod = method, + KeyModifiers = keyModifiers, + }); + + _textInputManager.SetFocusedElement(element); + RaisePropertyChanged(nameof(FocusedElement)); + } + } + + protected void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public void ProcessRawEvent(RawInputEventArgs e) + { + if(e.Handled) + return; + + var element = FocusedElement ?? e.Root; + + if (e is RawKeyEventArgs keyInput) + { + switch (keyInput.Type) + { + case RawKeyEventType.KeyDown: + case RawKeyEventType.KeyUp: + var routedEvent = keyInput.Type == RawKeyEventType.KeyDown + ? InputElement.KeyDownEvent + : InputElement.KeyUpEvent; + + KeyEventArgs ev = new KeyEventArgs + { + RoutedEvent = routedEvent, + Device = this, + Key = keyInput.Key, + KeyModifiers = keyInput.Modifiers.ToKeyModifiers(), + Source = element, + }; + + IVisual? currentHandler = element; + while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown) + { + var bindings = (currentHandler as IInputElement)?.KeyBindings; + if (bindings != null) + { + KeyBinding[]? bindingsCopy = null; + + // Create a copy of the KeyBindings list if there's a binding which matches the event. + // If we don't do this the foreach loop will throw an InvalidOperationException when the KeyBindings list is changed. + // This can happen when a new view is loaded which adds its own KeyBindings to the handler. + foreach (var binding in bindings) + { + if (binding.Gesture?.Matches(ev) == true) + { + bindingsCopy = bindings.ToArray(); + break; + } + } + + if (bindingsCopy is object) + { + foreach (var binding in bindingsCopy) + { + if (ev.Handled) + break; + binding.TryHandle(ev); + } + } + } + currentHandler = currentHandler.VisualParent; + } + + element.RaiseEvent(ev); + e.Handled = ev.Handled; + break; + } + } + + if (e is RawTextInputEventArgs text) + { + var ev = new TextInputEventArgs() + { + Device = this, + Text = text.Text, + Source = element, + RoutedEvent = InputElement.TextInputEvent + }; + + element.RaiseEvent(ev); + e.Handled = ev.Handled; + } + } + } +} diff --git a/src/Avalonia.Base/Input/KeyboardNavigation.cs b/src/Avalonia.Base/Input/KeyboardNavigation.cs new file mode 100644 index 0000000000..042a9adf5f --- /dev/null +++ b/src/Avalonia.Base/Input/KeyboardNavigation.cs @@ -0,0 +1,134 @@ +namespace Avalonia.Input +{ + /// + /// Defines attached properties that control keyboard navigation behaviour for a container. + /// + public static class KeyboardNavigation + { + /// + /// Defines the TabIndex attached property. + /// + public static readonly AttachedProperty TabIndexProperty = + AvaloniaProperty.RegisterAttached( + "TabIndex", + typeof(KeyboardNavigation), + int.MaxValue); + + /// + /// Defines the TabNavigation attached property. + /// + /// + /// The TabNavigation attached property defines how pressing the Tab key causes focus to + /// be navigated between the children of the container. + /// + public static readonly AttachedProperty TabNavigationProperty = + AvaloniaProperty.RegisterAttached( + "TabNavigation", + typeof(KeyboardNavigation)); + + /// + /// Defines the TabOnceActiveElement attached property. + /// + /// + /// When focus enters a container which has its + /// attached property set to , this property + /// defines to which child the focus should move. + /// + public static readonly AttachedProperty TabOnceActiveElementProperty = + AvaloniaProperty.RegisterAttached( + "TabOnceActiveElement", + typeof(KeyboardNavigation)); + + /// + /// Defines the IsTabStop attached property. + /// + /// + /// The IsTabStop attached property determines whether the control is focusable by tab navigation. + /// + public static readonly AttachedProperty IsTabStopProperty = + AvaloniaProperty.RegisterAttached( + "IsTabStop", + typeof(KeyboardNavigation), + true); + + /// + /// Gets the for an element. + /// + /// The container. + /// The for the container. + public static int GetTabIndex(IInputElement element) + { + return ((AvaloniaObject)element).GetValue(TabIndexProperty); + } + + /// + /// Sets the for an element. + /// + /// The element. + /// The tab index. + public static void SetTabIndex(IInputElement element, int value) + { + ((IAvaloniaObject)element).SetValue(TabIndexProperty, value); + } + + /// + /// Gets the for a container. + /// + /// The container. + /// The for the container. + public static KeyboardNavigationMode GetTabNavigation(InputElement element) + { + return element.GetValue(TabNavigationProperty); + } + + /// + /// Sets the for a container. + /// + /// The container. + /// The for the container. + public static void SetTabNavigation(InputElement element, KeyboardNavigationMode value) + { + element.SetValue(TabNavigationProperty, value); + } + + /// + /// Gets the for a container. + /// + /// The container. + /// The active element for the container. + public static IInputElement? GetTabOnceActiveElement(InputElement element) + { + return element.GetValue(TabOnceActiveElementProperty); + } + + /// + /// Sets the for a container. + /// + /// The container. + /// The active element for the container. + public static void SetTabOnceActiveElement(InputElement element, IInputElement? value) + { + element.SetValue(TabOnceActiveElementProperty, value); + } + + /// + /// Sets the for an element. + /// + /// The container. + /// Value indicating whether the container is a tab stop. + public static void SetIsTabStop(InputElement element, bool value) + { + element.SetValue(IsTabStopProperty, value); + } + + /// + /// Gets the for an element. + /// + /// The container. + /// Whether the container is a tab stop. + public static bool GetIsTabStop(InputElement element) + { + return element.GetValue(IsTabStopProperty); + } + } +} diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs similarity index 100% rename from src/Avalonia.Input/KeyboardNavigationHandler.cs rename to src/Avalonia.Base/Input/KeyboardNavigationHandler.cs diff --git a/src/Avalonia.Input/KeyboardNavigationMode.cs b/src/Avalonia.Base/Input/KeyboardNavigationMode.cs similarity index 100% rename from src/Avalonia.Input/KeyboardNavigationMode.cs rename to src/Avalonia.Base/Input/KeyboardNavigationMode.cs diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs new file mode 100644 index 0000000000..5f8ab24b79 --- /dev/null +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -0,0 +1,345 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Input.Raw; +using Avalonia.Interactivity; +using Avalonia.Platform; +using Avalonia.Utilities; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + /// + /// Represents a mouse device. + /// + public class MouseDevice : IMouseDevice, IDisposable + { + private int _clickCount; + private Rect _lastClickRect; + private ulong _lastClickTime; + + private readonly Pointer _pointer; + private bool _disposed; + private PixelPoint? _position; + private MouseButton _lastMouseDownButton; + + public MouseDevice(Pointer? pointer = null) + { + _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + } + + [Obsolete("Use IPointer instead")] + public IInputElement? Captured => _pointer.Captured; + + [Obsolete("Use events instead")] + public PixelPoint Position + { + get => _position ?? new PixelPoint(-1, -1); + protected set => _position = value; + } + + [Obsolete("Use IPointer instead")] + public void Capture(IInputElement? control) + { + _pointer.Capture(control); + } + + /// + /// Gets the mouse position relative to a control. + /// + /// The control. + /// The mouse position in the control's coordinates. + public Point GetPosition(IVisual relativeTo) + { + relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); + + if (relativeTo.VisualRoot == null) + { + throw new InvalidOperationException("Control is not attached to visual tree."); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var rootPoint = relativeTo.VisualRoot.PointToClient(Position); +#pragma warning restore CS0618 // Type or member is obsolete + var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); + return rootPoint * transform!.Value; + } + + public void ProcessRawEvent(RawInputEventArgs e) + { + if (!e.Handled && e is RawPointerEventArgs margs) + ProcessRawEvent(margs); + } + + int ButtonCount(PointerPointProperties props) + { + var rv = 0; + if (props.IsLeftButtonPressed) + rv++; + if (props.IsMiddleButtonPressed) + rv++; + if (props.IsRightButtonPressed) + rv++; + if (props.IsXButton1Pressed) + rv++; + if (props.IsXButton2Pressed) + rv++; + return rv; + } + + private void ProcessRawEvent(RawPointerEventArgs e) + { + e = e ?? throw new ArgumentNullException(nameof(e)); + + var mouse = (MouseDevice)e.Device; + if(mouse._disposed) + return; + + _position = e.Root.PointToScreen(e.Position); + var props = CreateProperties(e); + var keyModifiers = e.InputModifiers.ToKeyModifiers(); + switch (e.Type) + { + case RawPointerEventType.LeaveWindow: + case RawPointerEventType.NonClientLeftButtonDown: + LeaveWindow(); + break; + case RawPointerEventType.LeftButtonDown: + case RawPointerEventType.RightButtonDown: + case RawPointerEventType.MiddleButtonDown: + case RawPointerEventType.XButton1Down: + case RawPointerEventType.XButton2Down: + if (ButtonCount(props) > 1) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult); + else + e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); + break; + case RawPointerEventType.LeftButtonUp: + case RawPointerEventType.RightButtonUp: + case RawPointerEventType.MiddleButtonUp: + case RawPointerEventType.XButton1Up: + case RawPointerEventType.XButton2Up: + if (ButtonCount(props) != 0) + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult); + else + e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); + break; + case RawPointerEventType.Move: + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult); + break; + case RawPointerEventType.Wheel: + e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); + break; + case RawPointerEventType.Magnify: + e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); + break; + case RawPointerEventType.Rotate: + e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); + break; + case RawPointerEventType.Swipe: + e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); + break; + } + } + + private void LeaveWindow() + { + _position = null; + } + + PointerPointProperties CreateProperties(RawPointerEventArgs args) + { + return new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()); + } + + private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, + PointerPointProperties properties, + KeyModifiers inputModifiers, IInputElement? hitTest) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var source = _pointer.Captured ?? root.InputHitTest(p); + + if (source != null) + { + _pointer.Capture(source); + if (source != null) + { + var settings = AvaloniaLocator.Current.GetService(); + var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; + var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); + + if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) + { + _clickCount = 0; + } + + ++_clickCount; + _lastClickTime = timestamp; + _lastClickRect = new Rect(p, new Size()) + .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); + _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); + var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); + source.RaiseEvent(e); + return e.Handled; + } + } + + return false; + } + + private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties properties, KeyModifiers inputModifiers, Lazy?>? intermediatePoints, + IInputElement? hitTest) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var source = _pointer.Captured ?? hitTest; + + if (source is object) + { + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, + p, timestamp, properties, inputModifiers, intermediatePoints); + + source.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, + KeyModifiers inputModifiers, IInputElement? hitTest) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var source = _pointer.Captured ?? hitTest; + + if (source is not null) + { + var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, + _lastMouseDownButton); + + source?.RaiseEvent(e); + _pointer.Capture(null); + return e.Handled; + } + + return false; + } + + private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var source = _pointer.Captured ?? hitTest; + + // KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform. + // If Shift-Key is pressed and X is close to 0 we swap the Vector. + if (inputModifiers == KeyModifiers.Shift && MathUtilities.IsZero(delta.X)) + { + delta = new Vector(delta.Y, delta.X); + } + + if (source is not null) + { + var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var source = _pointer.Captured ?? hitTest; + + if (source != null) + { + var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source, + _pointer, root, p, timestamp, props, inputModifiers, delta); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var source = _pointer.Captured ?? hitTest; + + if (source != null) + { + var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source, + _pointer, root, p, timestamp, props, inputModifiers, delta); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) + { + device = device ?? throw new ArgumentNullException(nameof(device)); + root = root ?? throw new ArgumentNullException(nameof(root)); + + var source = _pointer.Captured ?? hitTest; + + if (source != null) + { + var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source, + _pointer, root, p, timestamp, props, inputModifiers, delta); + + source?.RaiseEvent(e); + return e.Handled; + } + + return false; + } + + public void Dispose() + { + _disposed = true; + _pointer?.Dispose(); + } + + [Obsolete] + public void TopLevelClosed(IInputRoot root) + { + // no-op + } + + [Obsolete] + public void SceneInvalidated(IInputRoot root, Rect rect) + { + // no-op + } + + public IPointer? TryGetPointer(RawPointerEventArgs ev) + { + return _pointer; + } + } +} diff --git a/src/Avalonia.Input/Navigation/FocusExtensions.cs b/src/Avalonia.Base/Input/Navigation/FocusExtensions.cs similarity index 100% rename from src/Avalonia.Input/Navigation/FocusExtensions.cs rename to src/Avalonia.Base/Input/Navigation/FocusExtensions.cs diff --git a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs new file mode 100644 index 0000000000..cfd45f8262 --- /dev/null +++ b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs @@ -0,0 +1,674 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.VisualTree; + +namespace Avalonia.Input.Navigation +{ + /// + /// The implementation for default tab navigation. + /// + internal static class TabNavigation + { + public static IInputElement? GetNextTab(IInputElement e, bool goDownOnly) + { + return GetNextTab(e, GetGroupParent(e), goDownOnly); + } + + public static IInputElement? GetNextTab(IInputElement? e, IInputElement container, bool goDownOnly) + { + var tabbingType = GetKeyNavigationMode(container); + + if (e == null) + { + if (IsTabStop(container)) + return container; + + // Using ActiveElement if set + var activeElement = GetActiveElement(container); + if (activeElement != null) + return GetNextTab(null, activeElement, true); + } + else + { + if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None) + { + if (container != e) + { + if (goDownOnly) + return null; + var parentContainer = GetGroupParent(container); + return GetNextTab(container, parentContainer, goDownOnly); + } + } + } + + // All groups + IInputElement? loopStartElement = null; + var nextTabElement = e; + var currentTabbingType = tabbingType; + + // Search down inside the container + while ((nextTabElement = GetNextTabInGroup(nextTabElement, container, currentTabbingType)) != null) + { + // Avoid the endless loop here for Cycle groups + if (loopStartElement == nextTabElement) + break; + if (loopStartElement == null) + loopStartElement = nextTabElement; + + var firstTabElementInside = GetNextTab(null, nextTabElement, true); + if (firstTabElementInside != null) + return firstTabElementInside; + + // If we want to continue searching inside the Once groups, we should change the navigation mode + if (currentTabbingType == KeyboardNavigationMode.Once) + currentTabbingType = KeyboardNavigationMode.Contained; + } + + // If there is no next element in the group (nextTabElement == null) + + // Search up in the tree if allowed + // consider: Use original tabbingType instead of currentTabbingType + if (!goDownOnly && currentTabbingType != KeyboardNavigationMode.Contained && GetParent(container) != null) + { + return GetNextTab(container, GetGroupParent(container), false); + } + + return null; + } + + public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e) + { + if (e is IInputElement container) + { + var last = GetLastInTree(container); + + if (last is object) + return GetNextTab(last, false); + } + + return null; + } + + public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly) + { + if (e is null && container is null) + throw new InvalidOperationException("Either 'e' or 'container' must be non-null."); + + if (container is null) + container = GetGroupParent(e!); + + KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container); + + if (e == null) + { + // Using ActiveElement if set + var activeElement = GetActiveElement(container); + if (activeElement != null) + return GetPrevTab(null, activeElement, true); + else + { + // If we Shift+Tab on a container with KeyboardNavigationMode=Once, and ActiveElement is null + // then we want to go to the first item (not last) within the container + if (tabbingType == KeyboardNavigationMode.Once) + { + var firstTabElement = GetNextTabInGroup(null, container, tabbingType); + if (firstTabElement == null) + { + if (IsTabStop(container)) + return container; + if (goDownOnly) + return null; + + return GetPrevTab(container, null, false); + } + else + { + return GetPrevTab(null, firstTabElement, true); + } + } + } + } + else + { + if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None) + { + if (goDownOnly || container == e) + return null; + + // FocusedElement should not be e otherwise we will delegate focus to the same element + if (IsTabStop(container)) + return container; + + return GetPrevTab(container, null, false); + } + } + + // All groups (except Once) - continue + IInputElement? loopStartElement = null; + IInputElement? nextTabElement = e; + + // Look for element with the same TabIndex before the current element + while ((nextTabElement = GetPrevTabInGroup(nextTabElement, container, tabbingType)) != null) + { + if (nextTabElement == container && tabbingType == KeyboardNavigationMode.Local) + break; + + // At this point nextTabElement is TabStop or TabGroup + // In case it is a TabStop only return the element + if (IsTabStop(nextTabElement) && !IsGroup(nextTabElement)) + return nextTabElement; + + // Avoid the endless loop here + if (loopStartElement == nextTabElement) + break; + if (loopStartElement == null) + loopStartElement = nextTabElement; + + // At this point nextTabElement is TabGroup + var lastTabElementInside = GetPrevTab(null, nextTabElement, true); + if (lastTabElementInside != null) + return lastTabElementInside; + } + + if (tabbingType == KeyboardNavigationMode.Contained) + return null; + + if (e != container && IsTabStop(container)) + return container; + + // If end of the subtree is reached or there no other elements above + if (!goDownOnly && GetParent(container) != null) + { + return GetPrevTab(container, null, false); + } + + return null; + } + + public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e) + { + if (e is IInputElement container) + { + var first = GetFirstChild(container); + + if (first is object) + return GetPrevTab(first, null, false); + } + + return null; + } + + private static IInputElement? FocusedElement(IInputElement e) + { + var iie = e; + // Focus delegation is enabled only if keyboard focus is outside the container + if (iie != null && !iie.IsKeyboardFocusWithin) + { + var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e); + if (focusedElement != null) + { + if (!IsFocusScope(e)) + { + // Verify if focusedElement is a visual descendant of e + if (focusedElement is IVisual visualFocusedElement && + visualFocusedElement != e && + e.IsVisualAncestorOf(visualFocusedElement)) + { + return focusedElement; + } + } + } + } + + return null; + } + + private static IInputElement? GetFirstChild(IInputElement e) + { + // If the element has a FocusedElement it should be its first child + if (FocusedElement(e) is IInputElement focusedElement) + return focusedElement; + + // Return the first visible element. + var uiElement = e as InputElement; + + if (uiElement is null || IsVisibleAndEnabled(uiElement)) + { + if (e is IVisual elementAsVisual) + { + var children = elementAsVisual.VisualChildren; + var count = children.Count; + + for (int i = 0; i < count; i++) + { + if (children[i] is InputElement ie) + { + if (IsVisibleAndEnabled(ie)) + return ie; + else + { + var firstChild = GetFirstChild(ie); + if (firstChild != null) + return firstChild; + } + } + } + } + } + + return null; + } + + private static IInputElement? GetLastChild(IInputElement e) + { + // If the element has a FocusedElement it should be its last child + if (FocusedElement(e) is IInputElement focusedElement) + return focusedElement; + + // Return the last visible element. + var uiElement = e as InputElement; + + if (uiElement == null || IsVisibleAndEnabled(uiElement)) + { + var elementAsVisual = e as IVisual; + + if (elementAsVisual != null) + { + var children = elementAsVisual.VisualChildren; + var count = children.Count; + + for (int i = count - 1; i >= 0; i--) + { + if (children[i] is InputElement ie) + { + if (IsVisibleAndEnabled(ie)) + return ie; + else + { + var lastChild = GetLastChild(ie); + if (lastChild != null) + return lastChild; + } + } + } + } + } + + return null; + } + + private static IInputElement? GetFirstTabInGroup(IInputElement container) + { + IInputElement? firstTabElement = null; + int minIndexFirstTab = int.MinValue; + + var currElement = container; + while ((currElement = GetNextInTree(currElement, container)) != null) + { + if (IsTabStopOrGroup(currElement)) + { + int currPriority = KeyboardNavigation.GetTabIndex(currElement); + + if (currPriority < minIndexFirstTab || firstTabElement == null) + { + minIndexFirstTab = currPriority; + firstTabElement = currElement; + } + } + } + return firstTabElement; + } + + private static IInputElement? GetLastInTree(IInputElement container) + { + IInputElement? result; + IInputElement? c = container; + + do + { + result = c; + c = GetLastChild(c); + } while (c != null && !IsGroup(c)); + + if (c != null) + return c; + + return result; + } + + private static IInputElement? GetLastTabInGroup(IInputElement container) + { + IInputElement? lastTabElement = null; + int maxIndexFirstTab = int.MaxValue; + var currElement = GetLastInTree(container); + while (currElement != null && currElement != container) + { + if (IsTabStopOrGroup(currElement)) + { + int currPriority = KeyboardNavigation.GetTabIndex(currElement); + + if (currPriority > maxIndexFirstTab || lastTabElement == null) + { + maxIndexFirstTab = currPriority; + lastTabElement = currElement; + } + } + currElement = GetPreviousInTree(currElement, container); + } + return lastTabElement; + } + + private static IInputElement? GetNextInTree(IInputElement e, IInputElement container) + { + IInputElement? result = null; + + if (e == container || !IsGroup(e)) + result = GetFirstChild(e); + + if (result != null || e == container) + return result; + + IInputElement? parent = e; + do + { + var sibling = GetNextSibling(parent); + if (sibling != null) + return sibling; + + parent = GetParent(parent); + } while (parent != null && parent != container); + + return null; + } + + private static IInputElement? GetNextSibling(IInputElement e) + { + if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual) + { + var children = parentAsVisual.VisualChildren; + var count = children.Count; + var i = 0; + + //go till itself + for (; i < count; i++) + { + var vchild = children[i]; + if (vchild == elementAsVisual) + break; + } + i++; + //search ahead + for (; i < count; i++) + { + var visual = children[i]; + if (visual is IInputElement ie) + return ie; + } + } + + return null; + } + + private static IInputElement? GetNextTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType) + { + // None groups: Tab navigation is not supported + if (tabbingType == KeyboardNavigationMode.None) + return null; + + // e == null or e == container -> return the first TabStopOrGroup + if (e == null || e == container) + { + return GetFirstTabInGroup(container); + } + + if (tabbingType == KeyboardNavigationMode.Once) + return null; + + var nextTabElement = GetNextTabWithSameIndex(e, container); + if (nextTabElement != null) + return nextTabElement; + + return GetNextTabWithNextIndex(e, container, tabbingType); + } + + private static IInputElement? GetNextTabWithSameIndex(IInputElement e, IInputElement container) + { + var elementTabPriority = KeyboardNavigation.GetTabIndex(e); + var currElement = e; + while ((currElement = GetNextInTree(currElement, container)) != null) + { + if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority) + { + return currElement; + } + } + + return null; + } + + private static IInputElement? GetNextTabWithNextIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType) + { + // Find the next min index in the tree + // min (index>currentTabIndex) + IInputElement? nextTabElement = null; + IInputElement? firstTabElement = null; + int minIndexFirstTab = int.MinValue; + int minIndex = int.MinValue; + int elementTabPriority = KeyboardNavigation.GetTabIndex(e); + + IInputElement? currElement = container; + while ((currElement = GetNextInTree(currElement, container)) != null) + { + if (IsTabStopOrGroup(currElement)) + { + int currPriority = KeyboardNavigation.GetTabIndex(currElement); + if (currPriority > elementTabPriority) + { + if (currPriority < minIndex || nextTabElement == null) + { + minIndex = currPriority; + nextTabElement = currElement; + } + } + + if (currPriority < minIndexFirstTab || firstTabElement == null) + { + minIndexFirstTab = currPriority; + firstTabElement = currElement; + } + } + } + + // Cycle groups: if not found - return first element + if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null) + nextTabElement = firstTabElement; + + return nextTabElement; + } + + private static IInputElement? GetPrevTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType) + { + // None groups: Tab navigation is not supported + if (tabbingType == KeyboardNavigationMode.None) + return null; + + // Search the last index inside the group + if (e == null) + { + return GetLastTabInGroup(container); + } + + if (tabbingType == KeyboardNavigationMode.Once) + return null; + + if (e == container) + return null; + + var nextTabElement = GetPrevTabWithSameIndex(e, container); + if (nextTabElement != null) + return nextTabElement; + + return GetPrevTabWithPrevIndex(e, container, tabbingType); + } + + private static IInputElement? GetPrevTabWithSameIndex(IInputElement e, IInputElement container) + { + int elementTabPriority = KeyboardNavigation.GetTabIndex(e); + var currElement = GetPreviousInTree(e, container); + while (currElement != null) + { + if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority && currElement != container) + { + return currElement; + } + currElement = GetPreviousInTree(currElement, container); + } + return null; + } + + private static IInputElement? GetPrevTabWithPrevIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType) + { + // Find the next max index in the tree + // max (index maxIndex || nextTabElement == null) + { + maxIndex = currPriority; + nextTabElement = currElement; + } + } + + if (currPriority > maxIndexFirstTab || lastTabElement == null) + { + maxIndexFirstTab = currPriority; + lastTabElement = currElement; + } + } + + currElement = GetPreviousInTree(currElement, container); + } + + // Cycle groups: if not found - return first element + if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null) + nextTabElement = lastTabElement; + + return nextTabElement; + } + + private static IInputElement? GetPreviousInTree(IInputElement e, IInputElement container) + { + if (e == container) + return null; + + var result = GetPreviousSibling(e); + + if (result != null) + { + if (IsGroup(result)) + return result; + else + return GetLastInTree(result); + } + else + return GetParent(e); + } + + private static IInputElement? GetPreviousSibling(IInputElement e) + { + if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual) + { + var children = parentAsVisual.VisualChildren; + var count = children.Count; + IInputElement? prev = null; + + for (int i = 0; i < count; i++) + { + var vchild = children[i]; + if (vchild == elementAsVisual) + break; + if (vchild is IInputElement ie && IsVisibleAndEnabled(ie)) + prev = ie; + } + return prev; + } + return null; + } + + private static IInputElement? GetActiveElement(IInputElement e) + { + return ((AvaloniaObject)e).GetValue(KeyboardNavigation.TabOnceActiveElementProperty); + } + + private static IInputElement GetGroupParent(IInputElement e) => GetGroupParent(e, false); + + private static IInputElement GetGroupParent(IInputElement element, bool includeCurrent) + { + var result = element; // Keep the last non null element + var e = element; + + // If we don't want to include the current element, + // start at the parent of the element. If the element + // is the root, then just return it as the group parent. + if (!includeCurrent) + { + result = e; + e = GetParent(e); + if (e == null) + return result; + } + + while (e != null) + { + if (IsGroup(e)) + return e; + + result = e; + e = GetParent(e); + } + + return result; + } + + private static IInputElement? GetParent(IInputElement e) + { + // For Visual - go up the visual parent chain until we find Visual. + if (e is IVisual v) + return v.FindAncestorOfType(); + + // This will need to be implemented when we have non-visual input elements. + throw new NotSupportedException(); + } + + private static KeyboardNavigationMode GetKeyNavigationMode(IInputElement e) + { + return ((AvaloniaObject)e).GetValue(KeyboardNavigation.TabNavigationProperty); + } + + private static bool IsFocusScope(IInputElement e) => FocusManager.GetIsFocusScope(e) || GetParent(e) == null; + private static bool IsGroup(IInputElement e) => GetKeyNavigationMode(e) != KeyboardNavigationMode.Continue; + + private static bool IsTabStop(IInputElement e) + { + if (e is InputElement ie) + return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEnabled; + return false; + } + + private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e); + private static bool IsVisibleAndEnabled(IInputElement e) => e.IsVisible && e.IsEnabled; + } +} diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Base/Input/NavigationDirection.cs similarity index 100% rename from src/Avalonia.Input/NavigationDirection.cs rename to src/Avalonia.Base/Input/NavigationDirection.cs diff --git a/src/Avalonia.Input/NavigationMethod.cs b/src/Avalonia.Base/Input/NavigationMethod.cs similarity index 100% rename from src/Avalonia.Input/NavigationMethod.cs rename to src/Avalonia.Base/Input/NavigationMethod.cs diff --git a/src/Avalonia.Input/Platform/IClipboard.cs b/src/Avalonia.Base/Input/Platform/IClipboard.cs similarity index 100% rename from src/Avalonia.Input/Platform/IClipboard.cs rename to src/Avalonia.Base/Input/Platform/IClipboard.cs diff --git a/src/Avalonia.Input/Platform/IPlatformDragSource.cs b/src/Avalonia.Base/Input/Platform/IPlatformDragSource.cs similarity index 100% rename from src/Avalonia.Input/Platform/IPlatformDragSource.cs rename to src/Avalonia.Base/Input/Platform/IPlatformDragSource.cs diff --git a/src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs b/src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs similarity index 100% rename from src/Avalonia.Input/Platform/PlatformHotkeyConfiguration.cs rename to src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs similarity index 100% rename from src/Avalonia.Input/Pointer.cs rename to src/Avalonia.Base/Input/Pointer.cs diff --git a/src/Avalonia.Input/PointerDeltaEventArgs.cs b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs similarity index 100% rename from src/Avalonia.Input/PointerDeltaEventArgs.cs rename to src/Avalonia.Base/Input/PointerDeltaEventArgs.cs diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs new file mode 100644 index 0000000000..5495802920 --- /dev/null +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using Avalonia.Input.Raw; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class PointerEventArgs : RoutedEventArgs + { + private readonly IVisual? _rootVisual; + private readonly Point _rootVisualPosition; + private readonly PointerPointProperties _properties; + private Lazy?>? _previousPoints; + + public PointerEventArgs(RoutedEvent routedEvent, + IInteractive? source, + IPointer pointer, + IVisual? rootVisual, Point rootVisualPosition, + ulong timestamp, + PointerPointProperties properties, + KeyModifiers modifiers) + : base(routedEvent) + { + Source = source; + _rootVisual = rootVisual; + _rootVisualPosition = rootVisualPosition; + _properties = properties; + Pointer = pointer; + Timestamp = timestamp; + KeyModifiers = modifiers; + } + + public PointerEventArgs(RoutedEvent routedEvent, + IInteractive? source, + IPointer pointer, + IVisual? rootVisual, Point rootVisualPosition, + ulong timestamp, + PointerPointProperties properties, + KeyModifiers modifiers, + Lazy?>? previousPoints) + : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) + { + _previousPoints = previousPoints; + } + + + class EmulatedDevice : IPointerDevice + { + private readonly PointerEventArgs _ev; + + public EmulatedDevice(PointerEventArgs ev) + { + _ev = ev; + } + + public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException(); + + public IInputElement? Captured => _ev.Pointer.Captured; + public void Capture(IInputElement? control) + { + _ev.Pointer.Capture(control); + } + + public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); + + public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer; + } + + public IPointer Pointer { get; } + public ulong Timestamp { get; } + + private IPointerDevice? _device; + + [Obsolete("Use Pointer to get pointer-specific information")] + public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this)); + + [Obsolete("Use KeyModifiers and PointerPointProperties")] + public InputModifiers InputModifiers + { + get + { + var mods = (InputModifiers)KeyModifiers; + if (_properties.IsLeftButtonPressed) + mods |= InputModifiers.LeftMouseButton; + if (_properties.IsMiddleButtonPressed) + mods |= InputModifiers.MiddleMouseButton; + if (_properties.IsRightButtonPressed) + mods |= InputModifiers.RightMouseButton; + + return mods; + } + } + + public KeyModifiers KeyModifiers { get; } + + private Point GetPosition(Point pt, IVisual? relativeTo) + { + if (_rootVisual == null) + return default; + if (relativeTo == null) + return pt; + return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; + } + + public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); + + [Obsolete("Use GetCurrentPoint")] + public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo); + + /// + /// Returns the PointerPoint associated with the current event + /// + /// The visual which coordinate system to use. Pass null for toplevel coordinate system + /// + public PointerPoint GetCurrentPoint(IVisual? relativeTo) + => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); + + /// + /// Returns the PointerPoint associated with the current event + /// + /// The visual which coordinate system to use. Pass null for toplevel coordinate system + /// + public IReadOnlyList GetIntermediatePoints(IVisual? relativeTo) + { + var previousPoints = _previousPoints?.Value; + if (previousPoints == null || previousPoints.Count == 0) + return new[] { GetCurrentPoint(relativeTo) }; + var points = new PointerPoint[previousPoints.Count + 1]; + for (var c = 0; c < previousPoints.Count; c++) + { + var pt = previousPoints[c]; + points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), _properties); + } + + points[points.Length - 1] = GetCurrentPoint(relativeTo); + return points; + } + + /// + /// Returns the current pointer point properties + /// + protected PointerPointProperties Properties => _properties; + } + + public enum MouseButton + { + None, + Left, + Right, + Middle, + XButton1, + XButton2 + } + + public class PointerPressedEventArgs : PointerEventArgs + { + private readonly int _clickCount; + + public PointerPressedEventArgs( + IInteractive source, + IPointer pointer, + IVisual rootVisual, Point rootVisualPosition, + ulong timestamp, + PointerPointProperties properties, + KeyModifiers modifiers, + int clickCount = 1) + : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) + { + _clickCount = clickCount; + } + + public int ClickCount => _clickCount; + + [Obsolete("Use PointerPressedEventArgs.GetCurrentPoint(this).Properties")] + public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton(); + } + + public class PointerReleasedEventArgs : PointerEventArgs + { + public PointerReleasedEventArgs( + IInteractive source, IPointer pointer, + IVisual rootVisual, Point rootVisualPosition, ulong timestamp, + PointerPointProperties properties, KeyModifiers modifiers, + MouseButton initialPressMouseButton) + : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) + { + InitialPressMouseButton = initialPressMouseButton; + } + + /// + /// Gets the mouse button that triggered the corresponding PointerPressed event + /// + public MouseButton InitialPressMouseButton { get; } + + [Obsolete("Use InitialPressMouseButton")] + public MouseButton MouseButton => InitialPressMouseButton; + } + + public class PointerCaptureLostEventArgs : RoutedEventArgs + { + public IPointer Pointer { get; } + + public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) + { + Pointer = pointer; + Source = source; + } + } +} diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs new file mode 100644 index 0000000000..d22252893d --- /dev/null +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -0,0 +1,209 @@ +using System; +using Avalonia.Input.Raw; + +namespace Avalonia.Input +{ + internal class PointerOverPreProcessor : IObserver + { + private IPointerDevice? _lastActivePointerDevice; + private (IPointer pointer, PixelPoint position)? _lastPointer; + + private readonly IInputRoot _inputRoot; + + public PointerOverPreProcessor(IInputRoot inputRoot) + { + _inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot)); + } + + public void OnCompleted() + { + ClearPointerOver(); + } + + public void OnError(Exception error) + { + } + + public void OnNext(RawInputEventArgs value) + { + if (value is RawPointerEventArgs args + && args.Root == _inputRoot + && value.Device is IPointerDevice pointerDevice) + { + if (pointerDevice != _lastActivePointerDevice) + { + ClearPointerOver(); + + // Set last active device before processing input, because ClearPointerOver might be called and clear last device. + _lastActivePointerDevice = pointerDevice; + } + + if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown + && _lastPointer is (var lastPointer, var lastPosition)) + { + _lastPointer = null; + ClearPointerOver(lastPointer, args.Root, 0, args.Root.PointToClient(lastPosition), + new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), + args.InputModifiers.ToKeyModifiers()); + } + else if (pointerDevice.TryGetPointer(args) is IPointer pointer + && pointer.Type != PointerType.Touch) + { + var element = pointer.Captured ?? args.InputHitTestResult; + + SetPointerOver(pointer, args.Root, element, args.Timestamp, args.Position, + new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), + args.InputModifiers.ToKeyModifiers()); + } + } + } + + public void SceneInvalidated(Rect dirtyRect) + { + if (_lastPointer is (var pointer, var position)) + { + var clientPoint = _inputRoot.PointToClient(position); + + if (dirtyRect.Contains(clientPoint)) + { + SetPointerOver(pointer, _inputRoot, _inputRoot.InputHitTest(clientPoint), 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); + } + else if (!_inputRoot.Bounds.Contains(clientPoint)) + { + ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None); + } + } + } + + private void ClearPointerOver() + { + if (_lastPointer is (var pointer, var _)) + { + ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None); + } + _lastPointer = null; + _lastActivePointerDevice = null; + } + + private void ClearPointerOver(IPointer pointer, IInputRoot root, + ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) + { + var element = root.PointerOverElement; + if (element is null) + { + return; + } + + // Do not pass rootVisual, when we have unknown (negative) position, + // so GetPosition won't return invalid values. + var hasPosition = position.X >= 0 && position.Y >= 0; + var e = new PointerEventArgs(InputElement.PointerLeaveEvent, element, pointer, + hasPosition ? root : null, hasPosition ? position : default, + timestamp, properties, inputModifiers); + + if (element != null && !element.IsAttachedToVisualTree) + { + // element has been removed from visual tree so do top down cleanup + if (root.IsPointerOver) + { + ClearChildrenPointerOver(e, root, true); + } + } + while (element != null) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + element = (IInputElement?)element.VisualParent; + } + + root.PointerOverElement = null; + _lastActivePointerDevice = null; + _lastPointer = null; + } + + private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot) + { + foreach (IInputElement el in element.VisualChildren) + { + if (el.IsPointerOver) + { + ClearChildrenPointerOver(e, el, true); + break; + } + } + if (clearRoot) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + } + } + + private void SetPointerOver(IPointer pointer, IInputRoot root, IInputElement? element, + ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) + { + var pointerOverElement = root.PointerOverElement; + + if (element != pointerOverElement) + { + if (element != null) + { + SetPointerOverToElement(pointer, root, element, timestamp, position, properties, inputModifiers); + } + else + { + ClearPointerOver(pointer, root, timestamp, position, properties, inputModifiers); + } + } + } + + private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element, + ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) + { + IInputElement? branch = null; + + IInputElement? el = element; + + while (el != null) + { + if (el.IsPointerOver) + { + branch = el; + break; + } + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement; + + var e = new PointerEventArgs(InputElement.PointerLeaveEvent, el, pointer, root, position, + timestamp, properties, inputModifiers); + if (el != null && branch != null && !el.IsAttachedToVisualTree) + { + ClearChildrenPointerOver(e, branch, false); + } + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement = element; + _lastPointer = (pointer, root.PointToScreen(position)); + + e.RoutedEvent = InputElement.PointerEnterEvent; + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + } + } +} diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Base/Input/PointerPoint.cs similarity index 100% rename from src/Avalonia.Input/PointerPoint.cs rename to src/Avalonia.Base/Input/PointerPoint.cs diff --git a/src/Avalonia.Input/PointerWheelEventArgs.cs b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs similarity index 100% rename from src/Avalonia.Input/PointerWheelEventArgs.cs rename to src/Avalonia.Base/Input/PointerWheelEventArgs.cs diff --git a/src/Avalonia.Input/Raw/IDragDropDevice.cs b/src/Avalonia.Base/Input/Raw/IDragDropDevice.cs similarity index 100% rename from src/Avalonia.Input/Raw/IDragDropDevice.cs rename to src/Avalonia.Base/Input/Raw/IDragDropDevice.cs diff --git a/src/Avalonia.Base/Input/Raw/RawDragEvent.cs b/src/Avalonia.Base/Input/Raw/RawDragEvent.cs new file mode 100644 index 0000000000..652bad7115 --- /dev/null +++ b/src/Avalonia.Base/Input/Raw/RawDragEvent.cs @@ -0,0 +1,29 @@ +using System; + +namespace Avalonia.Input.Raw +{ + public class RawDragEvent : RawInputEventArgs + { + public Point Location { get; set; } + public IDataObject Data { get; } + public DragDropEffects Effects { get; set; } + public RawDragEventType Type { get; } + [Obsolete("Use KeyModifiers")] + public InputModifiers Modifiers { get; } + public KeyModifiers KeyModifiers { get; } + + public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, + IInputRoot root, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) + :base(inputDevice, 0, root) + { + Type = type; + Location = location; + Data = data; + Effects = effects; + KeyModifiers = modifiers.ToKeyModifiers(); +#pragma warning disable CS0618 // Type or member is obsolete + Modifiers = (InputModifiers)modifiers; +#pragma warning restore CS0618 // Type or member is obsolete + } + } +} diff --git a/src/Avalonia.Input/Raw/RawDragEventType.cs b/src/Avalonia.Base/Input/Raw/RawDragEventType.cs similarity index 100% rename from src/Avalonia.Input/Raw/RawDragEventType.cs rename to src/Avalonia.Base/Input/Raw/RawDragEventType.cs diff --git a/src/Avalonia.Input/Raw/RawInputEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawInputEventArgs.cs similarity index 100% rename from src/Avalonia.Input/Raw/RawInputEventArgs.cs rename to src/Avalonia.Base/Input/Raw/RawInputEventArgs.cs diff --git a/src/Avalonia.Base/Input/Raw/RawInputHelpers.cs b/src/Avalonia.Base/Input/Raw/RawInputHelpers.cs new file mode 100644 index 0000000000..9d329bae59 --- /dev/null +++ b/src/Avalonia.Base/Input/Raw/RawInputHelpers.cs @@ -0,0 +1,27 @@ +using Avalonia.Input.Raw; + +namespace Avalonia.Input +{ + internal static class RawInputHelpers + { + public static KeyModifiers ToKeyModifiers(this RawInputModifiers modifiers) => + (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); + + public static PointerUpdateKind ToUpdateKind(this RawPointerEventType type) => type switch + { + RawPointerEventType.LeftButtonDown => PointerUpdateKind.LeftButtonPressed, + RawPointerEventType.LeftButtonUp => PointerUpdateKind.LeftButtonReleased, + RawPointerEventType.RightButtonDown => PointerUpdateKind.RightButtonPressed, + RawPointerEventType.RightButtonUp => PointerUpdateKind.RightButtonReleased, + RawPointerEventType.MiddleButtonDown => PointerUpdateKind.MiddleButtonPressed, + RawPointerEventType.MiddleButtonUp => PointerUpdateKind.MiddleButtonReleased, + RawPointerEventType.XButton1Down => PointerUpdateKind.XButton1Pressed, + RawPointerEventType.XButton1Up => PointerUpdateKind.XButton1Released, + RawPointerEventType.XButton2Down => PointerUpdateKind.XButton2Pressed, + RawPointerEventType.XButton2Up => PointerUpdateKind.XButton2Released, + RawPointerEventType.TouchBegin => PointerUpdateKind.LeftButtonPressed, + RawPointerEventType.TouchEnd => PointerUpdateKind.LeftButtonReleased, + _ => PointerUpdateKind.Other + }; + } +} diff --git a/src/Avalonia.Input/Raw/RawKeyEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawKeyEventArgs.cs similarity index 100% rename from src/Avalonia.Input/Raw/RawKeyEventArgs.cs rename to src/Avalonia.Base/Input/Raw/RawKeyEventArgs.cs diff --git a/src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawMouseWheelEventArgs.cs similarity index 100% rename from src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs rename to src/Avalonia.Base/Input/Raw/RawMouseWheelEventArgs.cs diff --git a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs new file mode 100644 index 0000000000..8b9d7c161d --- /dev/null +++ b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Input.Raw +{ + public enum RawPointerEventType + { + LeaveWindow, + LeftButtonDown, + LeftButtonUp, + RightButtonDown, + RightButtonUp, + MiddleButtonDown, + MiddleButtonUp, + XButton1Down, + XButton1Up, + XButton2Down, + XButton2Up, + Move, + Wheel, + NonClientLeftButtonDown, + TouchBegin, + TouchUpdate, + TouchEnd, + TouchCancel, + Magnify, + Rotate, + Swipe + } + + /// + /// A raw mouse event. + /// + public class RawPointerEventArgs : RawInputEventArgs + { + private RawPointerPoint _point; + + /// + /// Initializes a new instance of the class. + /// + /// The associated device. + /// The event timestamp. + /// The root from which the event originates. + /// The type of the event. + /// The mouse position, in client DIPs. + /// The input modifiers. + public RawPointerEventArgs( + IInputDevice device, + ulong timestamp, + IInputRoot root, + RawPointerEventType type, + Point position, + RawInputModifiers inputModifiers) + : base(device, timestamp, root) + { + Contract.Requires(device != null); + Contract.Requires(root != null); + + Position = position; + Type = type; + InputModifiers = inputModifiers; + } + + /// + /// Initializes a new instance of the class. + /// + /// The associated device. + /// The event timestamp. + /// The root from which the event originates. + /// The type of the event. + /// The point properties and position, in client DIPs. + /// The input modifiers. + public RawPointerEventArgs( + IInputDevice device, + ulong timestamp, + IInputRoot root, + RawPointerEventType type, + RawPointerPoint point, + RawInputModifiers inputModifiers) + : base(device, timestamp, root) + { + Contract.Requires(device != null); + Contract.Requires(root != null); + + Point = point; + Type = type; + InputModifiers = inputModifiers; + } + + /// + /// Gets the pointer properties and position, in client DIPs. + /// + public RawPointerPoint Point + { + get => _point; + set => _point = value; + } + + /// + /// Gets the mouse position, in client DIPs. + /// + public Point Position + { + get => _point.Position; + set => _point.Position = value; + } + + /// + /// Gets the type of the event. + /// + public RawPointerEventType Type { get; set; } + + /// + /// Gets the input modifiers. + /// + public RawInputModifiers InputModifiers { get; set; } + + /// + /// Points that were traversed by a pointer since the previous relevant event, + /// only valid for Move and TouchUpdate + /// + public Lazy?>? IntermediatePoints { get; set; } + + internal IInputElement? InputHitTestResult { get; set; } + } + + public struct RawPointerPoint + { + /// + /// Pointer position, in client DIPs. + /// + public Point Position { get; set; } + + public RawPointerPoint() + { + Position = default; + } + } +} diff --git a/src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerGestureEventArgs.cs similarity index 100% rename from src/Avalonia.Input/Raw/RawPointerGestureEventArgs.cs rename to src/Avalonia.Base/Input/Raw/RawPointerGestureEventArgs.cs diff --git a/src/Avalonia.Input/Raw/RawSizeEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawSizeEventArgs.cs similarity index 100% rename from src/Avalonia.Input/Raw/RawSizeEventArgs.cs rename to src/Avalonia.Base/Input/Raw/RawSizeEventArgs.cs diff --git a/src/Avalonia.Input/Raw/RawTextInputEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs similarity index 100% rename from src/Avalonia.Input/Raw/RawTextInputEventArgs.cs rename to src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs diff --git a/src/Avalonia.Input/Raw/RawTouchEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs similarity index 100% rename from src/Avalonia.Input/Raw/RawTouchEventArgs.cs rename to src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs diff --git a/src/Avalonia.Input/ScrollGestureEventArgs.cs b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs similarity index 100% rename from src/Avalonia.Input/ScrollGestureEventArgs.cs rename to src/Avalonia.Base/Input/ScrollGestureEventArgs.cs diff --git a/src/Avalonia.Input/TappedEventArgs.cs b/src/Avalonia.Base/Input/TappedEventArgs.cs similarity index 100% rename from src/Avalonia.Input/TappedEventArgs.cs rename to src/Avalonia.Base/Input/TappedEventArgs.cs diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs similarity index 100% rename from src/Avalonia.Input/TextInput/ITextInputMethodClient.cs rename to src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs similarity index 100% rename from src/Avalonia.Input/TextInput/ITextInputMethodImpl.cs rename to src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs diff --git a/src/Avalonia.Input/TextInput/InputMethodManager.cs b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs similarity index 100% rename from src/Avalonia.Input/TextInput/InputMethodManager.cs rename to src/Avalonia.Base/Input/TextInput/InputMethodManager.cs diff --git a/src/Avalonia.Input/TextInput/TextInputContentType.cs b/src/Avalonia.Base/Input/TextInput/TextInputContentType.cs similarity index 100% rename from src/Avalonia.Input/TextInput/TextInputContentType.cs rename to src/Avalonia.Base/Input/TextInput/TextInputContentType.cs diff --git a/src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs b/src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequestedEventArgs.cs similarity index 100% rename from src/Avalonia.Input/TextInput/TextInputMethodClientRequestedEventArgs.cs rename to src/Avalonia.Base/Input/TextInput/TextInputMethodClientRequestedEventArgs.cs diff --git a/src/Avalonia.Input/TextInput/TextInputOptions.cs b/src/Avalonia.Base/Input/TextInput/TextInputOptions.cs similarity index 100% rename from src/Avalonia.Input/TextInput/TextInputOptions.cs rename to src/Avalonia.Base/Input/TextInput/TextInputOptions.cs diff --git a/src/Avalonia.Input/TextInput/TransformTrackingHelper.cs b/src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs similarity index 100% rename from src/Avalonia.Input/TextInput/TransformTrackingHelper.cs rename to src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs diff --git a/src/Avalonia.Input/TextInputEventArgs.cs b/src/Avalonia.Base/Input/TextInputEventArgs.cs similarity index 100% rename from src/Avalonia.Input/TextInputEventArgs.cs rename to src/Avalonia.Base/Input/TextInputEventArgs.cs diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs new file mode 100644 index 0000000000..54dcc4051e --- /dev/null +++ b/src/Avalonia.Base/Input/TouchDevice.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + /// + /// Handles raw touch events + /// + /// + /// This class is supposed to be used on per-toplevel basis, don't use a shared one + /// + public class TouchDevice : IPointerDevice, IDisposable + { + private readonly Dictionary _pointers = new Dictionary(); + private bool _disposed; + private int _clickCount; + private Rect _lastClickRect; + private ulong _lastClickTime; + private Pointer? _lastPointer; + + IInputElement? IPointerDevice.Captured => _lastPointer?.Captured; + + RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown) + { + var rv = modifiers &= RawInputModifiers.KeyboardMask; + if (isLeftButtonDown) + rv |= RawInputModifiers.LeftMouseButton; + return rv; + } + + void IPointerDevice.Capture(IInputElement? control) => _lastPointer?.Capture(control); + + Point IPointerDevice.GetPosition(IVisual relativeTo) => default; + + public void ProcessRawEvent(RawInputEventArgs ev) + { + if (ev.Handled || _disposed) + return; + var args = (RawTouchEventArgs)ev; + if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) + { + if (args.Type == RawPointerEventType.TouchEnd) + return; + var hit = args.InputHitTestResult; + + _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), + PointerType.Touch, _pointers.Count == 0); + pointer.Capture(hit); + } + _lastPointer = pointer; + + var target = pointer.Captured ?? args.Root; + var updateKind = args.Type.ToUpdateKind(); + var keyModifier = args.InputModifiers.ToKeyModifiers(); + + if (args.Type == RawPointerEventType.TouchBegin) + { + if (_pointers.Count > 1) + { + _clickCount = 1; + _lastClickTime = 0; + _lastClickRect = new Rect(); + } + else + { + var settings = AvaloniaLocator.Current.GetRequiredService(); + + if (!_lastClickRect.Contains(args.Position) + || ev.Timestamp - _lastClickTime > settings.TouchDoubleClickTime.TotalMilliseconds) + { + _clickCount = 0; + } + ++_clickCount; + _lastClickTime = ev.Timestamp; + _lastClickRect = new Rect(args.Position, new Size()) + .Inflate(new Thickness(settings.TouchDoubleClickSize.Width / 2, settings.TouchDoubleClickSize.Height / 2)); + } + + target.RaiseEvent(new PointerPressedEventArgs(target, pointer, + args.Root, args.Position, ev.Timestamp, + new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind), + keyModifier, _clickCount)); + } + + if (args.Type == RawPointerEventType.TouchEnd) + { + _pointers.Remove(args.TouchPointId); + using (pointer) + { + target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, + args.Root, args.Position, ev.Timestamp, + new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind), + keyModifier, MouseButton.Left)); + } + _lastPointer = null; + } + + if (args.Type == RawPointerEventType.TouchCancel) + { + _pointers.Remove(args.TouchPointId); + using (pointer) + pointer.Capture(null); + _lastPointer = null; + } + + if (args.Type == RawPointerEventType.TouchUpdate) + { + target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, + args.Position, ev.Timestamp, + new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind), + keyModifier, args.IntermediatePoints)); + } + } + + public void Dispose() + { + if (_disposed) + return; + var values = _pointers.Values.ToArray(); + _pointers.Clear(); + _disposed = true; + foreach (var p in values) + p.Dispose(); + } + + public IPointer? TryGetPointer(RawPointerEventArgs ev) + { + return ev is RawTouchEventArgs args + && _pointers.TryGetValue(args.TouchPointId, out var pointer) + ? pointer + : null; + } + } +} diff --git a/src/Avalonia.Input/VectorEventArgs.cs b/src/Avalonia.Base/Input/VectorEventArgs.cs similarity index 100% rename from src/Avalonia.Input/VectorEventArgs.cs rename to src/Avalonia.Base/Input/VectorEventArgs.cs diff --git a/src/Avalonia.Interactivity/EventRoute.cs b/src/Avalonia.Base/Interactivity/EventRoute.cs similarity index 100% rename from src/Avalonia.Interactivity/EventRoute.cs rename to src/Avalonia.Base/Interactivity/EventRoute.cs diff --git a/src/Avalonia.Base/Interactivity/IInteractive.cs b/src/Avalonia.Base/Interactivity/IInteractive.cs new file mode 100644 index 0000000000..6d7dcd64f4 --- /dev/null +++ b/src/Avalonia.Base/Interactivity/IInteractive.cs @@ -0,0 +1,51 @@ +using System; + +#nullable enable + +namespace Avalonia.Interactivity +{ + /// + /// Interface for objects that raise routed events. + /// + public interface IInteractive + { + /// + /// Gets the interactive parent of the object for bubbling and tunneling events. + /// + IInteractive? InteractiveParent { get; } + + /// + /// Adds a handler for the specified routed event. + /// + /// The routed event. + /// The handler. + /// The routing strategies to listen to. + /// Whether handled events should also be listened for. + /// A disposable that terminates the event subscription. + void AddHandler( + RoutedEvent routedEvent, + Delegate handler, + RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, + bool handledEventsToo = false); + + /// + /// Removes a handler for the specified routed event. + /// + /// The routed event. + /// The handler. + void RemoveHandler(RoutedEvent routedEvent, Delegate handler); + + /// + /// Adds the object's handlers for a routed event to an event route. + /// + /// The event. + /// The event route. + void AddToEventRoute(RoutedEvent routedEvent, EventRoute route); + + /// + /// Raises a routed event. + /// + /// The event args. + void RaiseEvent(RoutedEventArgs e); + } +} diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Base/Interactivity/Interactive.cs similarity index 100% rename from src/Avalonia.Interactivity/Interactive.cs rename to src/Avalonia.Base/Interactivity/Interactive.cs diff --git a/src/Avalonia.Interactivity/InteractiveExtensions.cs b/src/Avalonia.Base/Interactivity/InteractiveExtensions.cs similarity index 100% rename from src/Avalonia.Interactivity/InteractiveExtensions.cs rename to src/Avalonia.Base/Interactivity/InteractiveExtensions.cs diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Base/Interactivity/RoutedEvent.cs similarity index 100% rename from src/Avalonia.Interactivity/RoutedEvent.cs rename to src/Avalonia.Base/Interactivity/RoutedEvent.cs diff --git a/src/Avalonia.Interactivity/RoutedEventArgs.cs b/src/Avalonia.Base/Interactivity/RoutedEventArgs.cs similarity index 100% rename from src/Avalonia.Interactivity/RoutedEventArgs.cs rename to src/Avalonia.Base/Interactivity/RoutedEventArgs.cs diff --git a/src/Avalonia.Interactivity/RoutedEventRegistry.cs b/src/Avalonia.Base/Interactivity/RoutedEventRegistry.cs similarity index 100% rename from src/Avalonia.Interactivity/RoutedEventRegistry.cs rename to src/Avalonia.Base/Interactivity/RoutedEventRegistry.cs diff --git a/src/Avalonia.Layout/AttachedLayout.cs b/src/Avalonia.Base/Layout/AttachedLayout.cs similarity index 100% rename from src/Avalonia.Layout/AttachedLayout.cs rename to src/Avalonia.Base/Layout/AttachedLayout.cs diff --git a/src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs b/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs similarity index 100% rename from src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs rename to src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Base/Layout/ElementManager.cs similarity index 100% rename from src/Avalonia.Layout/ElementManager.cs rename to src/Avalonia.Base/Layout/ElementManager.cs diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs similarity index 100% rename from src/Avalonia.Layout/FlowLayoutAlgorithm.cs rename to src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs diff --git a/src/Avalonia.Layout/IEmbeddedLayoutRoot.cs b/src/Avalonia.Base/Layout/IEmbeddedLayoutRoot.cs similarity index 100% rename from src/Avalonia.Layout/IEmbeddedLayoutRoot.cs rename to src/Avalonia.Base/Layout/IEmbeddedLayoutRoot.cs diff --git a/src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Base/Layout/IFlowLayoutAlgorithmDelegates.cs similarity index 100% rename from src/Avalonia.Layout/IFlowLayoutAlgorithmDelegates.cs rename to src/Avalonia.Base/Layout/IFlowLayoutAlgorithmDelegates.cs diff --git a/src/Avalonia.Layout/ILayoutManager.cs b/src/Avalonia.Base/Layout/ILayoutManager.cs similarity index 100% rename from src/Avalonia.Layout/ILayoutManager.cs rename to src/Avalonia.Base/Layout/ILayoutManager.cs diff --git a/src/Avalonia.Layout/ILayoutRoot.cs b/src/Avalonia.Base/Layout/ILayoutRoot.cs similarity index 100% rename from src/Avalonia.Layout/ILayoutRoot.cs rename to src/Avalonia.Base/Layout/ILayoutRoot.cs diff --git a/src/Avalonia.Layout/ILayoutable.cs b/src/Avalonia.Base/Layout/ILayoutable.cs similarity index 100% rename from src/Avalonia.Layout/ILayoutable.cs rename to src/Avalonia.Base/Layout/ILayoutable.cs diff --git a/src/Avalonia.Layout/LayoutContext.cs b/src/Avalonia.Base/Layout/LayoutContext.cs similarity index 100% rename from src/Avalonia.Layout/LayoutContext.cs rename to src/Avalonia.Base/Layout/LayoutContext.cs diff --git a/src/Avalonia.Layout/LayoutContextAdapter.cs b/src/Avalonia.Base/Layout/LayoutContextAdapter.cs similarity index 100% rename from src/Avalonia.Layout/LayoutContextAdapter.cs rename to src/Avalonia.Base/Layout/LayoutContextAdapter.cs diff --git a/src/Avalonia.Layout/LayoutExtensions.cs b/src/Avalonia.Base/Layout/LayoutExtensions.cs similarity index 100% rename from src/Avalonia.Layout/LayoutExtensions.cs rename to src/Avalonia.Base/Layout/LayoutExtensions.cs diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Base/Layout/LayoutHelper.cs similarity index 100% rename from src/Avalonia.Layout/LayoutHelper.cs rename to src/Avalonia.Base/Layout/LayoutHelper.cs diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs similarity index 100% rename from src/Avalonia.Layout/LayoutManager.cs rename to src/Avalonia.Base/Layout/LayoutManager.cs diff --git a/src/Avalonia.Layout/LayoutQueue.cs b/src/Avalonia.Base/Layout/LayoutQueue.cs similarity index 100% rename from src/Avalonia.Layout/LayoutQueue.cs rename to src/Avalonia.Base/Layout/LayoutQueue.cs diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs similarity index 100% rename from src/Avalonia.Layout/Layoutable.cs rename to src/Avalonia.Base/Layout/Layoutable.cs diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Base/Layout/NonVirtualizingLayout.cs similarity index 100% rename from src/Avalonia.Layout/NonVirtualizingLayout.cs rename to src/Avalonia.Base/Layout/NonVirtualizingLayout.cs diff --git a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Base/Layout/NonVirtualizingLayoutContext.cs similarity index 100% rename from src/Avalonia.Layout/NonVirtualizingLayoutContext.cs rename to src/Avalonia.Base/Layout/NonVirtualizingLayoutContext.cs diff --git a/src/Avalonia.Layout/NonVirtualizingStackLayout.cs b/src/Avalonia.Base/Layout/NonVirtualizingStackLayout.cs similarity index 100% rename from src/Avalonia.Layout/NonVirtualizingStackLayout.cs rename to src/Avalonia.Base/Layout/NonVirtualizingStackLayout.cs diff --git a/src/Avalonia.Layout/Orientation.cs b/src/Avalonia.Base/Layout/Orientation.cs similarity index 100% rename from src/Avalonia.Layout/Orientation.cs rename to src/Avalonia.Base/Layout/Orientation.cs diff --git a/src/Avalonia.Layout/OrientationBasedMeasures.cs b/src/Avalonia.Base/Layout/OrientationBasedMeasures.cs similarity index 100% rename from src/Avalonia.Layout/OrientationBasedMeasures.cs rename to src/Avalonia.Base/Layout/OrientationBasedMeasures.cs diff --git a/src/Avalonia.Base/Layout/StackLayout.cs b/src/Avalonia.Base/Layout/StackLayout.cs new file mode 100644 index 0000000000..00ac4a37f0 --- /dev/null +++ b/src/Avalonia.Base/Layout/StackLayout.cs @@ -0,0 +1,363 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections.Specialized; +using Avalonia.Data; +using Avalonia.Logging; + +namespace Avalonia.Layout +{ + /// + /// Arranges elements into a single line (with spacing) that can be oriented horizontally or vertically. + /// + public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates + { + /// + /// Defines the property. + /// + public static readonly StyledProperty DisableVirtualizationProperty = + AvaloniaProperty.Register(nameof(DisableVirtualization)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OrientationProperty = + AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SpacingProperty = + AvaloniaProperty.Register(nameof(Spacing)); + + private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); + + /// + /// Initializes a new instance of the StackLayout class. + /// + public StackLayout() + { + LayoutId = "StackLayout"; + } + + /// + /// Gets or sets a value indicating whether virtualization is disabled on the layout. + /// + public bool DisableVirtualization + { + get => GetValue(DisableVirtualizationProperty); + set => SetValue(DisableVirtualizationProperty, value); + } + + /// + /// Gets or sets the axis along which items are laid out. + /// + /// + /// One of the enumeration values that specifies the axis along which items are laid out. + /// The default is Vertical. + /// + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the + /// direction of the StackLayout's Orientation. + /// + public double Spacing + { + get => GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + internal Rect GetExtent( + Size availableSize, + VirtualizingLayoutContext context, + ILayoutable? firstRealized, + int firstRealizedItemIndex, + Rect firstRealizedLayoutBounds, + ILayoutable? lastRealized, + int lastRealizedItemIndex, + Rect lastRealizedLayoutBounds) + { + var extent = new Rect(); + + // Constants + int itemsCount = context.ItemCount; + var stackState = (StackLayoutState)context.LayoutState!; + double averageElementSize = GetAverageElementSize(availableSize, context, stackState) + Spacing; + + _orientation.SetMinorSize(ref extent, stackState.MaxArrangeBounds); + _orientation.SetMajorSize(ref extent, Math.Max(0.0f, itemsCount * averageElementSize - Spacing)); + if (itemsCount > 0) + { + if (firstRealized != null) + { + _orientation.SetMajorStart( + ref extent, + _orientation.MajorStart(firstRealizedLayoutBounds) - firstRealizedItemIndex * averageElementSize); + var remainingItems = itemsCount - lastRealizedItemIndex - 1; + _orientation.SetMajorSize( + ref extent, + _orientation.MajorEnd(lastRealizedLayoutBounds) - + _orientation.MajorStart(extent) + + (remainingItems * averageElementSize)); + } + else + { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", + LayoutId); + } + } + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on average {Average}", + LayoutId, extent.Size, averageElementSize); + return extent; + } + + internal void OnElementMeasured( + ILayoutable element, + int index, + Size availableSize, + Size measureSize, + Size desiredSize, + Size provisionalArrangeSize, + VirtualizingLayoutContext context) + { + if (context is VirtualizingLayoutContext virtualContext) + { + var stackState = (StackLayoutState)virtualContext.LayoutState!; + var provisionalArrangeSizeWinRt = provisionalArrangeSize; + stackState.OnElementMeasured( + index, + _orientation.Major(provisionalArrangeSizeWinRt), + _orientation.Minor(provisionalArrangeSizeWinRt)); + } + } + + Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize( + int index, + Size availableSize, + VirtualizingLayoutContext context) => availableSize; + + Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize( + int index, + Size measureSize, + Size desiredSize, + VirtualizingLayoutContext context) + { + var measureSizeMinor = _orientation.Minor(measureSize); + return _orientation.MinorMajorSize( + !double.IsInfinity(measureSizeMinor) ? + Math.Max(measureSizeMinor, _orientation.Minor(desiredSize)) : + _orientation.Minor(desiredSize), + _orientation.Major(desiredSize)); + } + + bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => true; + + FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect( + Size availableSize, + VirtualizingLayoutContext context) => GetAnchorForRealizationRect(availableSize, context); + + FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement( + int targetIndex, + Size availableSize, + VirtualizingLayoutContext context) + { + double offset = double.NaN; + int index = -1; + int itemsCount = context.ItemCount; + + if (targetIndex >= 0 && targetIndex < itemsCount) + { + index = targetIndex; + var state = (StackLayoutState)context.LayoutState!; + double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing; + offset = index * averageElementSize + _orientation.MajorStart(state.FlowAlgorithm.LastExtent); + } + + return new FlowLayoutAnchorInfo { Index = index, Offset = offset }; + } + + Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( + Size availableSize, + VirtualizingLayoutContext context, + ILayoutable? firstRealized, + int firstRealizedItemIndex, + Rect firstRealizedLayoutBounds, + ILayoutable? lastRealized, + int lastRealizedItemIndex, + Rect lastRealizedLayoutBounds) + { + return GetExtent( + availableSize, + context, + firstRealized, + firstRealizedItemIndex, + firstRealizedLayoutBounds, + lastRealized, + lastRealizedItemIndex, + lastRealizedLayoutBounds); + } + + void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) + { + OnElementMeasured( + element, + index, + availableSize, + measureSize, + desiredSize, + provisionalArrangeSize, + context); + } + + void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context) + { + } + + internal FlowLayoutAnchorInfo GetAnchorForRealizationRect( + Size availableSize, + VirtualizingLayoutContext context) + { + int anchorIndex = -1; + double offset = double.NaN; + + // Constants + int itemsCount = context.ItemCount; + if (itemsCount > 0) + { + var realizationRect = context.RealizationRect; + var state = (StackLayoutState)context.LayoutState!; + var lastExtent = state.FlowAlgorithm.LastExtent; + + double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing; + double realizationWindowOffsetInExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); + double majorSize = _orientation.MajorSize(lastExtent) == 0 ? Math.Max(0.0, averageElementSize * itemsCount - Spacing) : _orientation.MajorSize(lastExtent); + if (itemsCount > 0 && + _orientation.MajorSize(realizationRect) >= 0 && + // MajorSize = 0 will account for when a nested repeater is outside the realization rect but still being measured. Also, + // note that if we are measuring this repeater, then we are already realizing an element to figure out the size, so we could + // just keep that element alive. It also helps in XYFocus scenarios to have an element realized for XYFocus to find a candidate + // in the navigating direction. + realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize) + { + anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize); + anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex)); + offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent); + } + } + + return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, }; + } + + protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) + { + var state = context.LayoutState; + var stackState = state as StackLayoutState; + + if (stackState == null) + { + if (state != null) + { + throw new InvalidOperationException("LayoutState must derive from StackLayoutState."); + } + + // Custom deriving layouts could potentially be stateful. + // If that is the case, we will just create the base state required by UniformGridLayout ourselves. + stackState = new StackLayoutState(); + } + + stackState.InitializeForContext(context, this); + } + + protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) + { + var stackState = (StackLayoutState)context.LayoutState!; + stackState.UninitializeForContext(context); + } + + protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + { + ((StackLayoutState)context.LayoutState!).OnMeasureStart(); + + var desiredSize = GetFlowAlgorithm(context).Measure( + availableSize, + context, + false, + 0, + Spacing, + int.MaxValue, + _orientation.ScrollOrientation, + DisableVirtualization, + LayoutId); + + return new Size(desiredSize.Width, desiredSize.Height); + } + + protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + { + var value = GetFlowAlgorithm(context).Arrange( + finalSize, + context, + false, + FlowLayoutAlgorithm.LineAlignment.Start, + LayoutId); + + return new Size(value.Width, value.Height); + } + + protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args) + { + GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context); + // Always invalidate layout to keep the view accurate. + InvalidateLayout(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == OrientationProperty) + { + var orientation = change.GetNewValue(); + + //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. + //Horizontal Orientation means we have a Horizontal ScrollOrientation. + _orientation.ScrollOrientation = orientation == Orientation.Horizontal ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical; + } + + InvalidateLayout(); + } + + private double GetAverageElementSize( + Size availableSize, + VirtualizingLayoutContext context, + StackLayoutState stackLayoutState) + { + double averageElementSize = 0; + + if (context.ItemCount > 0) + { + if (stackLayoutState.TotalElementsMeasured == 0) + { + var tmpElement = context.GetOrCreateElementAt(0, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); + stackLayoutState.FlowAlgorithm.MeasureElement(tmpElement, 0, availableSize, context); + context.RecycleElement(tmpElement); + } + + averageElementSize = Math.Round(stackLayoutState.TotalElementSize / stackLayoutState.TotalElementsMeasured); + } + + return averageElementSize; + } + + private void InvalidateLayout() => InvalidateMeasure(); + + private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((StackLayoutState)context.LayoutState!).FlowAlgorithm; + } +} diff --git a/src/Avalonia.Layout/StackLayoutState.cs b/src/Avalonia.Base/Layout/StackLayoutState.cs similarity index 100% rename from src/Avalonia.Layout/StackLayoutState.cs rename to src/Avalonia.Base/Layout/StackLayoutState.cs diff --git a/src/Avalonia.Base/Layout/UniformGridLayout.cs b/src/Avalonia.Base/Layout/UniformGridLayout.cs new file mode 100644 index 0000000000..418cd55e41 --- /dev/null +++ b/src/Avalonia.Base/Layout/UniformGridLayout.cs @@ -0,0 +1,561 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Collections.Specialized; +using Avalonia.Data; +using Avalonia.Logging; + +namespace Avalonia.Layout +{ + /// + /// Defines constants that specify how items are aligned on the non-scrolling or non-virtualizing axis. + /// + public enum UniformGridLayoutItemsJustification + { + /// + /// Items are aligned with the start of the row or column, with extra space at the end. + /// Spacing between items does not change. + /// + Start = 0, + + /// + /// Items are aligned in the center of the row or column, with extra space at the start and + /// end. Spacing between items does not change. + /// + Center = 1, + + /// + /// Items are aligned with the end of the row or column, with extra space at the start. + /// Spacing between items does not change. + /// + End = 2, + + /// + /// Items are aligned so that extra space is added evenly before and after each item. + /// + SpaceAround = 3, + + /// + /// Items are aligned so that extra space is added evenly between adjacent items. No space + /// is added at the start or end. + /// + SpaceBetween = 4, + + SpaceEvenly = 5, + }; + + /// + /// Defines constants that specify how items are sized to fill the available space. + /// + public enum UniformGridLayoutItemsStretch + { + /// + /// The item retains its natural size. Use of extra space is determined by the + /// property. + /// + None = 0, + + /// + /// The item is sized to fill the available space in the non-scrolling direction. Item size + /// in the scrolling direction is not changed. + /// + Fill = 1, + + /// + /// The item is sized to both fill the available space in the non-scrolling direction and + /// maintain its aspect ratio. + /// + Uniform = 2, + }; + + /// + /// Positions elements sequentially from left to right or top to bottom in a wrapping layout. + /// + public class UniformGridLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemsJustificationProperty = + AvaloniaProperty.Register(nameof(ItemsJustification)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemsStretchProperty = + AvaloniaProperty.Register(nameof(ItemsStretch)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinColumnSpacingProperty = + AvaloniaProperty.Register(nameof(MinColumnSpacing)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinItemHeightProperty = + AvaloniaProperty.Register(nameof(MinItemHeight)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinItemWidthProperty = + AvaloniaProperty.Register(nameof(MinItemWidth)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinRowSpacingProperty = + AvaloniaProperty.Register(nameof(MinRowSpacing)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumRowsOrColumnsProperty = + AvaloniaProperty.Register(nameof(MinItemWidth)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OrientationProperty = + StackLayout.OrientationProperty.AddOwner(); + + private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); + private double _minItemWidth = double.NaN; + private double _minItemHeight = double.NaN; + private double _minRowSpacing; + private double _minColumnSpacing; + private UniformGridLayoutItemsJustification _itemsJustification; + private UniformGridLayoutItemsStretch _itemsStretch; + private int _maximumRowsOrColumns = int.MaxValue; + + /// + /// Initializes a new instance of the class. + /// + public UniformGridLayout() + { + LayoutId = "UniformGridLayout"; + } + + static UniformGridLayout() + { + OrientationProperty.OverrideDefaultValue(Orientation.Horizontal); + } + + /// + /// Gets or sets a value that indicates how items are aligned on the non-scrolling or non- + /// virtualizing axis. + /// + /// + /// An enumeration value that indicates how items are aligned. The default is Start. + /// + public UniformGridLayoutItemsJustification ItemsJustification + { + get => GetValue(ItemsJustificationProperty); + set => SetValue(ItemsJustificationProperty, value); + } + + /// + /// Gets or sets a value that indicates how items are sized to fill the available space. + /// + /// + /// An enumeration value that indicates how items are sized to fill the available space. + /// The default is None. + /// + /// + /// This property enables adaptive layout behavior where the items are sized to fill the + /// available space along the non-scrolling axis, and optionally maintain their aspect ratio. + /// + public UniformGridLayoutItemsStretch ItemsStretch + { + get => GetValue(ItemsStretchProperty); + set => SetValue(ItemsStretchProperty, value); + } + + /// + /// Gets or sets the minimum space between items on the horizontal axis. + /// + /// + /// The spacing may exceed this minimum value when is set + /// to SpaceEvenly, SpaceAround, or SpaceBetween. + /// + public double MinColumnSpacing + { + get => GetValue(MinColumnSpacingProperty); + set => SetValue(MinColumnSpacingProperty, value); + } + + /// + /// Gets or sets the minimum height of each item. + /// + /// + /// The minimum height (in pixels) of each item. The default is NaN, in which case the + /// height of the first item is used as the minimum. + /// + public double MinItemHeight + { + get => GetValue(MinItemHeightProperty); + set => SetValue(MinItemHeightProperty, value); + } + + /// + /// Gets or sets the minimum width of each item. + /// + /// + /// The minimum width (in pixels) of each item. The default is NaN, in which case the width + /// of the first item is used as the minimum. + /// + public double MinItemWidth + { + get => GetValue(MinItemWidthProperty); + set => SetValue(MinItemWidthProperty, value); + } + + /// + /// Gets or sets the minimum space between items on the vertical axis. + /// + /// + /// The spacing may exceed this minimum value when is set + /// to SpaceEvenly, SpaceAround, or SpaceBetween. + /// + public double MinRowSpacing + { + get => GetValue(MinRowSpacingProperty); + set => SetValue(MinRowSpacingProperty, value); + } + + /// + /// Gets or sets the maximum row or column count. + /// + public int MaximumRowsOrColumns + { + get => GetValue(MaximumRowsOrColumnsProperty); + set => SetValue(MaximumRowsOrColumnsProperty, value); + } + + /// + /// Gets or sets the axis along which items are laid out. + /// + /// + /// One of the enumeration values that specifies the axis along which items are laid out. + /// The default is Vertical. + /// + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + internal double LineSpacing => Orientation == Orientation.Horizontal ? _minRowSpacing : _minColumnSpacing; + internal double MinItemSpacing => Orientation == Orientation.Horizontal ? _minColumnSpacing : _minRowSpacing; + + Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize( + int index, + Size availableSize, + VirtualizingLayoutContext context) + { + var gridState = (UniformGridLayoutState)context.LayoutState!; + return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight); + } + + Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize( + int index, + Size measureSize, + Size desiredSize, + VirtualizingLayoutContext context) + { + var gridState = (UniformGridLayoutState)context.LayoutState!; + return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight); + } + + bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => remainingSpace < 0; + + FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect( + Size availableSize, + VirtualizingLayoutContext context) + { + Rect bounds = new Rect(double.NaN, double.NaN, double.NaN, double.NaN); + int anchorIndex = -1; + + int itemsCount = context.ItemCount; + var realizationRect = context.RealizationRect; + if (itemsCount > 0 && _orientation.MajorSize(realizationRect) > 0) + { + var gridState = (UniformGridLayoutState)context.LayoutState!; + var lastExtent = gridState.FlowAlgorithm.LastExtent; + var itemsPerLine = Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, (uint)_maximumRowsOrColumns)); + var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); + var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); + if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize) + { + double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent)); + int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context)); + + anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); + bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context); + } + } + + return new FlowLayoutAnchorInfo + { + Index = anchorIndex, + Offset = _orientation.MajorStart(bounds) + }; + } + + FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement( + int targetIndex, + Size availableSize, + VirtualizingLayoutContext context) + { + int index = -1; + double offset = double.NaN; + int count = context.ItemCount; + if (targetIndex >= 0 && targetIndex < count) + { + int itemsPerLine = (int)Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); + int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine; + index = indexOfFirstInLine; + var state = (UniformGridLayoutState)context.LayoutState!; + offset = _orientation.MajorStart(GetLayoutRectForDataIndex(availableSize, indexOfFirstInLine, state.FlowAlgorithm.LastExtent, context)); + } + + return new FlowLayoutAnchorInfo + { + Index = index, + Offset = offset + }; + } + + Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( + Size availableSize, + VirtualizingLayoutContext context, + ILayoutable? firstRealized, + int firstRealizedItemIndex, + Rect firstRealizedLayoutBounds, + ILayoutable? lastRealized, + int lastRealizedItemIndex, + Rect lastRealizedLayoutBounds) + { + var extent = new Rect(); + + + // Constants + int itemsCount = context.ItemCount; + double availableSizeMinor = _orientation.Minor(availableSize); + int itemsPerLine = + (int)Math.Min( // note use of unsigned ints + Math.Max(1u, !double.IsInfinity(availableSizeMinor) + ? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context)) + : (uint)itemsCount), + Math.Max(1u, _maximumRowsOrColumns)); + double lineSize = GetMajorSizeWithSpacing(context); + + if (itemsCount > 0) + { + _orientation.SetMinorSize( + ref extent, + !double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ? + availableSizeMinor : + Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); + _orientation.SetMajorSize( + ref extent, + Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing)); + + if (firstRealized != null) + { + _orientation.SetMajorStart( + ref extent, + _orientation.MajorStart(firstRealizedLayoutBounds) - (firstRealizedItemIndex / itemsPerLine) * lineSize); + int remainingItems = itemsCount - lastRealizedItemIndex - 1; + _orientation.SetMajorSize( + ref extent, + _orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize); + } + else + { + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", LayoutId); + } + } + + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on lineSize {LineSize} and items per line {ItemsPerLine}", + LayoutId, extent.Size, lineSize, itemsPerLine); + return extent; + } + + void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) + { + } + + void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context) + { + } + + protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) + { + var state = context.LayoutState; + var gridState = state as UniformGridLayoutState; + + if (gridState == null) + { + if (state != null) + { + throw new InvalidOperationException("LayoutState must derive from UniformGridLayoutState."); + } + + // Custom deriving layouts could potentially be stateful. + // If that is the case, we will just create the base state required by UniformGridLayout ourselves. + gridState = new UniformGridLayoutState(); + } + + gridState.InitializeForContext(context, this); + } + + protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) + { + var gridState = (UniformGridLayoutState)context.LayoutState!; + gridState.UninitializeForContext(context); + } + + protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + { + // Set the width and height on the grid state. If the user already set them then use the preset. + // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items. + var gridState = (UniformGridLayoutState)context.LayoutState!; + gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns); + + var desiredSize = GetFlowAlgorithm(context).Measure( + availableSize, + context, + true, + MinItemSpacing, + LineSpacing, + _maximumRowsOrColumns, + _orientation.ScrollOrientation, + false, + LayoutId); + + // If after Measure the first item is in the realization rect, then we revoke grid state's ownership, + // and only use the layout when to clear it when it's done. + gridState.EnsureFirstElementOwnership(context); + + return desiredSize; + } + + protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + { + var value = GetFlowAlgorithm(context).Arrange( + finalSize, + context, + true, + (FlowLayoutAlgorithm.LineAlignment)_itemsJustification, + LayoutId); + return new Size(value.Width, value.Height); + } + + protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args) + { + GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context); + // Always invalidate layout to keep the view accurate. + InvalidateLayout(); + + var gridState = (UniformGridLayoutState)context.LayoutState!; + gridState.ClearElementOnDataSourceChange(context, args); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == OrientationProperty) + { + var orientation = change.GetNewValue(); + + //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. + //i.e. the properties are the inverse of each other. + var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; + _orientation.ScrollOrientation = scrollOrientation; + } + else if (change.Property == MinColumnSpacingProperty) + { + _minColumnSpacing = change.GetNewValue(); + } + else if (change.Property == MinRowSpacingProperty) + { + _minRowSpacing = change.GetNewValue(); + } + else if (change.Property == ItemsJustificationProperty) + { + _itemsJustification = change.GetNewValue(); + } + else if (change.Property == ItemsStretchProperty) + { + _itemsStretch = change.GetNewValue(); + } + else if (change.Property == MinItemWidthProperty) + { + _minItemWidth = change.GetNewValue(); + } + else if (change.Property == MinItemHeightProperty) + { + _minItemHeight = change.GetNewValue(); + } + else if (change.Property == MaximumRowsOrColumnsProperty) + { + _maximumRowsOrColumns = change.GetNewValue(); + } + + InvalidateLayout(); + } + + private double GetMinorSizeWithSpacing(VirtualizingLayoutContext context) + { + var minItemSpacing = MinItemSpacing; + var gridState = (UniformGridLayoutState)context.LayoutState!; + return _orientation.ScrollOrientation == ScrollOrientation.Vertical? + gridState.EffectiveItemWidth + minItemSpacing : + gridState.EffectiveItemHeight + minItemSpacing; + } + + private double GetMajorSizeWithSpacing(VirtualizingLayoutContext context) + { + var lineSpacing = LineSpacing; + var gridState = (UniformGridLayoutState)context.LayoutState!; + return _orientation.ScrollOrientation == ScrollOrientation.Vertical ? + gridState.EffectiveItemHeight + lineSpacing : + gridState.EffectiveItemWidth + lineSpacing; + } + + Rect GetLayoutRectForDataIndex( + Size availableSize, + int index, + Rect lastExtent, + VirtualizingLayoutContext context) + { + int itemsPerLine = (int)Math.Min( //note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); + int rowIndex = (int)(index / itemsPerLine); + int indexInRow = index - (rowIndex * itemsPerLine); + + var gridState = (UniformGridLayoutState)context.LayoutState!; + Rect bounds = _orientation.MinorMajorRect( + indexInRow * GetMinorSizeWithSpacing(context) + _orientation.MinorStart(lastExtent), + rowIndex * GetMajorSizeWithSpacing(context) + _orientation.MajorStart(lastExtent), + _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight, + _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth); + + return bounds; + } + + private void InvalidateLayout() => InvalidateMeasure(); + + private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((UniformGridLayoutState)context.LayoutState!).FlowAlgorithm; + } +} diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Base/Layout/UniformGridLayoutState.cs similarity index 100% rename from src/Avalonia.Layout/UniformGridLayoutState.cs rename to src/Avalonia.Base/Layout/UniformGridLayoutState.cs diff --git a/src/Avalonia.Layout/Utils/ListUtils.cs b/src/Avalonia.Base/Layout/Utils/ListUtils.cs similarity index 100% rename from src/Avalonia.Layout/Utils/ListUtils.cs rename to src/Avalonia.Base/Layout/Utils/ListUtils.cs diff --git a/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs b/src/Avalonia.Base/Layout/VirtualLayoutContextAdapter.cs similarity index 100% rename from src/Avalonia.Layout/VirtualLayoutContextAdapter.cs rename to src/Avalonia.Base/Layout/VirtualLayoutContextAdapter.cs diff --git a/src/Avalonia.Layout/VirtualizingLayout.cs b/src/Avalonia.Base/Layout/VirtualizingLayout.cs similarity index 100% rename from src/Avalonia.Layout/VirtualizingLayout.cs rename to src/Avalonia.Base/Layout/VirtualizingLayout.cs diff --git a/src/Avalonia.Layout/VirtualizingLayoutContext.cs b/src/Avalonia.Base/Layout/VirtualizingLayoutContext.cs similarity index 100% rename from src/Avalonia.Layout/VirtualizingLayoutContext.cs rename to src/Avalonia.Base/Layout/VirtualizingLayoutContext.cs diff --git a/src/Avalonia.Layout/WrapLayout/UvBounds.cs b/src/Avalonia.Base/Layout/WrapLayout/UvBounds.cs similarity index 100% rename from src/Avalonia.Layout/WrapLayout/UvBounds.cs rename to src/Avalonia.Base/Layout/WrapLayout/UvBounds.cs diff --git a/src/Avalonia.Layout/WrapLayout/UvMeasure.cs b/src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs similarity index 100% rename from src/Avalonia.Layout/WrapLayout/UvMeasure.cs rename to src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs diff --git a/src/Avalonia.Layout/WrapLayout/WrapItem.cs b/src/Avalonia.Base/Layout/WrapLayout/WrapItem.cs similarity index 100% rename from src/Avalonia.Layout/WrapLayout/WrapItem.cs rename to src/Avalonia.Base/Layout/WrapLayout/WrapItem.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs b/src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs new file mode 100644 index 0000000000..52de1dd3b8 --- /dev/null +++ b/src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs @@ -0,0 +1,336 @@ +// 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. + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Layout; +using System; +using System.Collections.Specialized; + +namespace Avalonia.Layout +{ + /// + /// Arranges elements by wrapping them to fit the available space. + /// When is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row. + /// When is set to Orientation.Vertical, element are arranged in columns until the available height is reached. + /// + public class WrapLayout : VirtualizingLayout + { + /// + /// Gets or sets a uniform Horizontal distance (in pixels) between items when is set to Horizontal, + /// or between columns of items when is set to Vertical. + /// + public double HorizontalSpacing + { + get { return (double)GetValue(HorizontalSpacingProperty); } + set { SetValue(HorizontalSpacingProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly StyledProperty HorizontalSpacingProperty = + AvaloniaProperty.Register(nameof(HorizontalSpacing), 0); + + /// + /// Gets or sets a uniform Vertical distance (in pixels) between items when is set to Vertical, + /// or between rows of items when is set to Horizontal. + /// + public double VerticalSpacing + { + get { return (double)GetValue(VerticalSpacingProperty); } + set { SetValue(VerticalSpacingProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly StyledProperty VerticalSpacingProperty = + AvaloniaProperty.Register( + nameof(VerticalSpacing), 0d); + + /// + /// Gets or sets the orientation of the WrapLayout. + /// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls. + /// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added. + /// + public Orientation Orientation + { + get { return (Orientation)GetValue(OrientationProperty); } + set { SetValue(OrientationProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly StyledProperty OrientationProperty = + AvaloniaProperty.Register( + nameof(Orientation), + Orientation.Horizontal); + + /// + protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) + { + var state = new WrapLayoutState(context); + context.LayoutState = state; + base.InitializeForContextCore(context); + } + + /// + protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) + { + context.LayoutState = null; + base.UninitializeForContextCore(context); + } + + /// + protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args) + { + var state = (WrapLayoutState)context.LayoutState!; + + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + state.RemoveFromIndex(args.NewStartingIndex); + break; + case NotifyCollectionChangedAction.Move: + int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex); + state.RemoveFromIndex(minIndex); + + state.RecycleElementAt(args.OldStartingIndex); + state.RecycleElementAt(args.NewStartingIndex); + break; + case NotifyCollectionChangedAction.Remove: + state.RemoveFromIndex(args.OldStartingIndex); + break; + case NotifyCollectionChangedAction.Replace: + state.RemoveFromIndex(args.NewStartingIndex); + state.RecycleElementAt(args.NewStartingIndex); + break; + case NotifyCollectionChangedAction.Reset: + state.Clear(); + break; + } + + base.OnItemsChangedCore(context, source, args); + } + + /// + protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + { + var totalMeasure = UvMeasure.Zero; + var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height); + var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); + var realizationBounds = new UvBounds(Orientation, context.RealizationRect); + var position = UvMeasure.Zero; + + var state = (WrapLayoutState)context.LayoutState!; + if (state.Orientation != Orientation) + { + state.SetOrientation(Orientation); + } + + if (spacingMeasure.Equals(state.Spacing) == false) + { + state.ClearPositions(); + state.Spacing = spacingMeasure; + } + + if (state.AvailableU != parentMeasure.U) + { + state.ClearPositions(); + state.AvailableU = parentMeasure.U; + } + + double currentV = 0; + for (int i = 0; i < context.ItemCount; i++) + { + bool measured = false; + WrapItem item = state.GetItemAt(i); + if (item.Measure == null) + { + item.Element = context.GetOrCreateElementAt(i); + item.Element.Measure(availableSize); + item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); + measured = true; + } + + UvMeasure currentMeasure = item.Measure.Value; + if (currentMeasure.U == 0) + { + continue; // ignore collapsed items + } + + if (item.Position == null) + { + if (parentMeasure.U < position.U + currentMeasure.U) + { + // New Row + position.U = 0; + position.V += currentV + spacingMeasure.V; + currentV = 0; + } + + item.Position = position; + } + + position = item.Position.Value; + + double vEnd = position.V + currentMeasure.V; + if (vEnd < realizationBounds.VMin) + { + // Item is "above" the bounds + if (item.Element != null) + { + context.RecycleElement(item.Element); + item.Element = null; + } + } + else if (position.V > realizationBounds.VMax) + { + // Item is "below" the bounds. + if (item.Element != null) + { + context.RecycleElement(item.Element); + item.Element = null; + } + + // We don't need to measure anything below the bounds + break; + } + else if (measured == false) + { + // Always measure elements that are within the bounds + item.Element = context.GetOrCreateElementAt(i); + item.Element.Measure(availableSize); + + currentMeasure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); + if (currentMeasure.Equals(item.Measure) == false) + { + // this item changed size; we need to recalculate layout for everything after this + state.RemoveFromIndex(i + 1); + item.Measure = currentMeasure; + + // did the change make it go into the new row? + if (parentMeasure.U < position.U + currentMeasure.U) + { + // New Row + position.U = 0; + position.V += currentV + spacingMeasure.V; + currentV = 0; + } + + item.Position = position; + } + } + + position.U += currentMeasure.U + spacingMeasure.U; + currentV = Math.Max(currentMeasure.V, currentV); + } + + // update value with the last line + // if the the last loop is(parentMeasure.U > currentMeasure.U + lineMeasure.U) the total isn't calculated then calculate it + // if the last loop is (parentMeasure.U > currentMeasure.U) the currentMeasure isn't added to the total so add it here + // for the last condition it is zeros so adding it will make no difference + // this way is faster than an if condition in every loop for checking the last item + totalMeasure.U = parentMeasure.U; + + // Propagating an infinite size causes a crash. This can happen if the parent is scrollable and infinite in the opposite + // axis to the panel. Clearing to zero prevents the crash. + // This is likely an incorrect use of the control by the developer, however we need stability here so setting a default that wont crash. + if (double.IsInfinity(totalMeasure.U)) + { + totalMeasure.U = 0.0; + } + + totalMeasure.V = state.GetHeight(); + + totalMeasure.U = Math.Ceiling(totalMeasure.U); + + return Orientation == Orientation.Horizontal ? new Size(totalMeasure.U, totalMeasure.V) : new Size(totalMeasure.V, totalMeasure.U); + } + + /// + protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + { + if (context.ItemCount > 0) + { + var parentMeasure = new UvMeasure(Orientation, finalSize.Width, finalSize.Height); + var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); + var realizationBounds = new UvBounds(Orientation, context.RealizationRect); + + var state = (WrapLayoutState)context.LayoutState!; + bool Arrange(WrapItem item, bool isLast = false) + { + if (item.Measure.HasValue == false) + { + return false; + } + + if (item.Position == null) + { + return false; + } + + var desiredMeasure = item.Measure.Value; + if (desiredMeasure.U == 0) + { + return true; // if an item is collapsed, avoid adding the spacing + } + + UvMeasure position = item.Position.Value; + + // Stretch the last item to fill the available space + if (isLast) + { + desiredMeasure.U = parentMeasure.U - position.U; + } + + if (((position.V + desiredMeasure.V) >= realizationBounds.VMin) && (position.V <= realizationBounds.VMax)) + { + // place the item + var child = context.GetOrCreateElementAt(item.Index); + if (Orientation == Orientation.Horizontal) + { + child.Arrange(new Rect(position.U, position.V, desiredMeasure.U, desiredMeasure.V)); + } + else + { + child.Arrange(new Rect(position.V, position.U, desiredMeasure.V, desiredMeasure.U)); + } + } + else if (position.V > realizationBounds.VMax) + { + return false; + } + + return true; + } + + for (var i = 0; i < context.ItemCount; i++) + { + bool continueArranging = Arrange(state.GetItemAt(i)); + if (continueArranging == false) + { + break; + } + } + } + + return finalSize; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == OrientationProperty || change.Property == HorizontalSpacingProperty || change.Property == VerticalSpacingProperty) + { + InvalidateMeasure(); + InvalidateArrange(); + } + } + } +} diff --git a/src/Avalonia.Layout/WrapLayout/WrapLayoutState.cs b/src/Avalonia.Base/Layout/WrapLayout/WrapLayoutState.cs similarity index 100% rename from src/Avalonia.Layout/WrapLayout/WrapLayoutState.cs rename to src/Avalonia.Base/Layout/WrapLayout/WrapLayoutState.cs diff --git a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs new file mode 100644 index 0000000000..497596fcc1 --- /dev/null +++ b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs @@ -0,0 +1,28 @@ +#nullable enable +using System; + +namespace Avalonia.LogicalTree +{ + /// + /// Event args for event. + /// + public class ChildIndexChangedEventArgs : EventArgs + { + public static new ChildIndexChangedEventArgs Empty { get; } = new ChildIndexChangedEventArgs(); + + private ChildIndexChangedEventArgs() + { + } + + public ChildIndexChangedEventArgs(ILogical child) + { + Child = child; + } + + /// + /// Logical child which index was changed. + /// If null, all children should be reset. + /// + public ILogical? Child { get; } + } +} diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Base/LogicalTree/ControlLocator.cs similarity index 100% rename from src/Avalonia.Styling/LogicalTree/ControlLocator.cs rename to src/Avalonia.Base/LogicalTree/ControlLocator.cs diff --git a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs similarity index 100% rename from src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs rename to src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs diff --git a/src/Avalonia.Styling/LogicalTree/ILogical.cs b/src/Avalonia.Base/LogicalTree/ILogical.cs similarity index 100% rename from src/Avalonia.Styling/LogicalTree/ILogical.cs rename to src/Avalonia.Base/LogicalTree/ILogical.cs diff --git a/src/Avalonia.Styling/LogicalTree/ILogicalRoot.cs b/src/Avalonia.Base/LogicalTree/ILogicalRoot.cs similarity index 100% rename from src/Avalonia.Styling/LogicalTree/ILogicalRoot.cs rename to src/Avalonia.Base/LogicalTree/ILogicalRoot.cs diff --git a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs b/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs similarity index 100% rename from src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs rename to src/Avalonia.Base/LogicalTree/LogicalExtensions.cs diff --git a/src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs b/src/Avalonia.Base/LogicalTree/LogicalTreeAttachmentEventArgs.cs similarity index 100% rename from src/Avalonia.Styling/LogicalTree/LogicalTreeAttachmentEventArgs.cs rename to src/Avalonia.Base/LogicalTree/LogicalTreeAttachmentEventArgs.cs diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Base/Matrix.cs similarity index 100% rename from src/Avalonia.Visuals/Matrix.cs rename to src/Avalonia.Base/Matrix.cs diff --git a/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs b/src/Avalonia.Base/Media/AcrylicBackgroundSource.cs similarity index 100% rename from src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs rename to src/Avalonia.Base/Media/AcrylicBackgroundSource.cs diff --git a/src/Avalonia.Visuals/Media/AlignmentX.cs b/src/Avalonia.Base/Media/AlignmentX.cs similarity index 100% rename from src/Avalonia.Visuals/Media/AlignmentX.cs rename to src/Avalonia.Base/Media/AlignmentX.cs diff --git a/src/Avalonia.Visuals/Media/AlignmentY.cs b/src/Avalonia.Base/Media/AlignmentY.cs similarity index 100% rename from src/Avalonia.Visuals/Media/AlignmentY.cs rename to src/Avalonia.Base/Media/AlignmentY.cs diff --git a/src/Avalonia.Visuals/Media/ArcSegment.cs b/src/Avalonia.Base/Media/ArcSegment.cs similarity index 100% rename from src/Avalonia.Visuals/Media/ArcSegment.cs rename to src/Avalonia.Base/Media/ArcSegment.cs diff --git a/src/Avalonia.Visuals/Media/BaselineAlignment.cs b/src/Avalonia.Base/Media/BaselineAlignment.cs similarity index 100% rename from src/Avalonia.Visuals/Media/BaselineAlignment.cs rename to src/Avalonia.Base/Media/BaselineAlignment.cs diff --git a/src/Avalonia.Visuals/Media/BezierSegment .cs b/src/Avalonia.Base/Media/BezierSegment .cs similarity index 100% rename from src/Avalonia.Visuals/Media/BezierSegment .cs rename to src/Avalonia.Base/Media/BezierSegment .cs diff --git a/src/Avalonia.Visuals/Media/BoxShadow.cs b/src/Avalonia.Base/Media/BoxShadow.cs similarity index 100% rename from src/Avalonia.Visuals/Media/BoxShadow.cs rename to src/Avalonia.Base/Media/BoxShadow.cs diff --git a/src/Avalonia.Visuals/Media/BoxShadows.cs b/src/Avalonia.Base/Media/BoxShadows.cs similarity index 100% rename from src/Avalonia.Visuals/Media/BoxShadows.cs rename to src/Avalonia.Base/Media/BoxShadows.cs diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Base/Media/Brush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Brush.cs rename to src/Avalonia.Base/Media/Brush.cs diff --git a/src/Avalonia.Visuals/Media/BrushConverter.cs b/src/Avalonia.Base/Media/BrushConverter.cs similarity index 100% rename from src/Avalonia.Visuals/Media/BrushConverter.cs rename to src/Avalonia.Base/Media/BrushConverter.cs diff --git a/src/Avalonia.Visuals/Media/BrushExtensions.cs b/src/Avalonia.Base/Media/BrushExtensions.cs similarity index 100% rename from src/Avalonia.Visuals/Media/BrushExtensions.cs rename to src/Avalonia.Base/Media/BrushExtensions.cs diff --git a/src/Avalonia.Visuals/Media/BrushMappingMode.cs b/src/Avalonia.Base/Media/BrushMappingMode.cs similarity index 100% rename from src/Avalonia.Visuals/Media/BrushMappingMode.cs rename to src/Avalonia.Base/Media/BrushMappingMode.cs diff --git a/src/Avalonia.Visuals/Media/Brushes.cs b/src/Avalonia.Base/Media/Brushes.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Brushes.cs rename to src/Avalonia.Base/Media/Brushes.cs diff --git a/src/Avalonia.Visuals/Media/CharacterHit.cs b/src/Avalonia.Base/Media/CharacterHit.cs similarity index 100% rename from src/Avalonia.Visuals/Media/CharacterHit.cs rename to src/Avalonia.Base/Media/CharacterHit.cs diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs new file mode 100644 index 0000000000..cb90404f6d --- /dev/null +++ b/src/Avalonia.Base/Media/Color.cs @@ -0,0 +1,790 @@ +// Color conversion portions of this source file are adapted from the WinUI project +// (https://github.com/microsoft/microsoft-ui-xaml) +// and the Windows Community Toolkit project. +// (https://github.com/CommunityToolkit/WindowsCommunityToolkit) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Globalization; +#if !BUILDTASK +using Avalonia.Animation.Animators; +#endif + +namespace Avalonia.Media +{ + /// + /// An ARGB color. + /// +#if !BUILDTASK + public +#endif + readonly struct Color : IEquatable + { + private const double byteToDouble = 1.0 / 255; + + static Color() + { +#if !BUILDTASK + Animation.Animation.RegisterAnimator(prop => typeof(Color).IsAssignableFrom(prop.PropertyType)); +#endif + } + + /// + /// Gets the Alpha component of the color. + /// + public byte A { get; } + + /// + /// Gets the Red component of the color. + /// + public byte R { get; } + + /// + /// Gets the Green component of the color. + /// + public byte G { get; } + + /// + /// Gets the Blue component of the color. + /// + public byte B { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The alpha component. + /// The red component. + /// The green component. + /// The blue component. + public Color(byte a, byte r, byte g, byte b) + { + A = a; + R = r; + G = g; + B = b; + } + + /// + /// Creates a from alpha, red, green and blue components. + /// + /// The alpha component. + /// The red component. + /// The green component. + /// The blue component. + /// The color. + public static Color FromArgb(byte a, byte r, byte g, byte b) + { + return new Color(a, r, g, b); + } + + /// + /// Creates a from red, green and blue components. + /// + /// The red component. + /// The green component. + /// The blue component. + /// The color. + public static Color FromRgb(byte r, byte g, byte b) + { + return new Color(0xff, r, g, b); + } + + /// + /// Creates a from an integer. + /// + /// The integer value. + /// The color. + public static Color FromUInt32(uint value) + { + return new Color( + (byte)((value >> 24) & 0xff), + (byte)((value >> 16) & 0xff), + (byte)((value >> 8) & 0xff), + (byte)(value & 0xff) + ); + } + + /// + /// Parses a color string. + /// + /// The color string. + /// The . + public static Color Parse(string s) + { + if (s is null) + { + throw new ArgumentNullException(nameof(s)); + } + + if (TryParse(s, out Color color)) + { + return color; + } + + throw new FormatException($"Invalid color string: '{s}'."); + } + + /// + /// Parses a color string. + /// + /// The color string. + /// The . + public static Color Parse(ReadOnlySpan s) + { + if (TryParse(s, out Color color)) + { + return color; + } + + throw new FormatException($"Invalid color string: '{s.ToString()}'."); + } + + /// + /// Parses a color string. + /// + /// The color string. + /// The parsed color + /// The status of the operation. + public static bool TryParse(string s, out Color color) + { + color = default; + + if (s is null) + { + return false; + } + + if (s.Length == 0) + { + return false; + } + + if (s[0] == '#' && + TryParseHexFormat(s.AsSpan(), out color)) + { + return true; + } + + // Note: The length checks are also an important optimization. + // The shortest possible CSS format is "rbg(0,0,0)", Length = 10. + + if (s.Length >= 10 && + (s[0] == 'r' || s[0] == 'R') && + (s[1] == 'g' || s[1] == 'G') && + (s[2] == 'b' || s[2] == 'B') && + TryParseCssFormat(s, out color)) + { + return true; + } + + if (s.Length >= 10 && + (s[0] == 'h' || s[0] == 'H') && + (s[1] == 's' || s[1] == 'S') && + (s[2] == 'l' || s[2] == 'L') && + HslColor.TryParse(s, out HslColor hslColor)) + { + color = hslColor.ToRgb(); + return true; + } + + if (s.Length >= 10 && + (s[0] == 'h' || s[0] == 'H') && + (s[1] == 's' || s[1] == 'S') && + (s[2] == 'v' || s[2] == 'V') && + HsvColor.TryParse(s, out HsvColor hsvColor)) + { + color = hsvColor.ToRgb(); + return true; + } + + var knownColor = KnownColors.GetKnownColor(s); + + if (knownColor != KnownColor.None) + { + color = knownColor.ToColor(); + return true; + } + + return false; + } + + /// + /// Parses a color string. + /// + /// The color string. + /// The parsed color + /// The status of the operation. + public static bool TryParse(ReadOnlySpan s, out Color color) + { + if (s.Length == 0) + { + color = default; + return false; + } + + if (s[0] == '#' && + TryParseHexFormat(s, out color)) + { + return true; + } + + // At this point all parsing uses strings + var str = s.ToString(); + + // Note: The length checks are also an important optimization. + // The shortest possible CSS format is "rbg(0,0,0)", Length = 10. + + if (s.Length >= 10 && + (s[0] == 'r' || s[0] == 'R') && + (s[1] == 'g' || s[1] == 'G') && + (s[2] == 'b' || s[2] == 'B') && + TryParseCssFormat(str, out color)) + { + return true; + } + + if (s.Length >= 10 && + (s[0] == 'h' || s[0] == 'H') && + (s[1] == 's' || s[1] == 'S') && + (s[2] == 'l' || s[2] == 'L') && + HslColor.TryParse(str, out HslColor hslColor)) + { + color = hslColor.ToRgb(); + return true; + } + + if (s.Length >= 10 && + (s[0] == 'h' || s[0] == 'H') && + (s[1] == 's' || s[1] == 'S') && + (s[2] == 'v' || s[2] == 'V') && + HsvColor.TryParse(str, out HsvColor hsvColor)) + { + color = hsvColor.ToRgb(); + return true; + } + + var knownColor = KnownColors.GetKnownColor(str); + + if (knownColor != KnownColor.None) + { + color = knownColor.ToColor(); + return true; + } + + color = default; + + return false; + } + + /// + /// Parses the given span of characters representing a hex color value into a new . + /// + private static bool TryParseHexFormat(ReadOnlySpan s, out Color color) + { + static bool TryParseCore(ReadOnlySpan input, ref Color color) + { + var alphaComponent = 0u; + + if (input.Length == 6) + { + alphaComponent = 0xff000000; + } + else if (input.Length != 8) + { + return false; + } + + // TODO: (netstandard 2.1) Can use allocation free parsing. + if (!uint.TryParse(input.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, + out var parsed)) + { + return false; + } + + color = FromUInt32(parsed | alphaComponent); + + return true; + } + + color = default; + + ReadOnlySpan input = s.Slice(1); + + // Handle shorthand cases like #FFF (RGB) or #FFFF (ARGB). + if (input.Length == 3 || input.Length == 4) + { + var extendedLength = 2 * input.Length; + +#if !BUILDTASK + Span extended = stackalloc char[extendedLength]; +#else + char[] extended = new char[extendedLength]; +#endif + + for (int i = 0; i < input.Length; i++) + { + extended[2 * i + 0] = input[i]; + extended[2 * i + 1] = input[i]; + } + + return TryParseCore(extended, ref color); + } + + return TryParseCore(input, ref color); + } + + /// + /// Parses the given string representing a CSS color value into a new . + /// + private static bool TryParseCssFormat(string s, out Color color) + { + bool prefixMatched = false; + + color = default; + + if (s is null) + { + return false; + } + + string workingString = s.Trim(); + + if (workingString.Length == 0 || + workingString.IndexOf(",", StringComparison.Ordinal) < 0) + { + return false; + } + + if (workingString.Length >= 11 && + workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) && + workingString.EndsWith(")", StringComparison.Ordinal)) + { + workingString = workingString.Substring(5, workingString.Length - 6); + prefixMatched = true; + } + + if (prefixMatched == false && + workingString.Length >= 10 && + workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) && + workingString.EndsWith(")", StringComparison.Ordinal)) + { + workingString = workingString.Substring(4, workingString.Length - 5); + prefixMatched = true; + } + + if (prefixMatched == false) + { + return false; + } + + string[] components = workingString.Split(','); + + if (components.Length == 3) // RGB + { + if (InternalTryParseByte(components[0], out byte red) && + InternalTryParseByte(components[1], out byte green) && + InternalTryParseByte(components[2], out byte blue)) + { + color = new Color(0xFF, red, green, blue); + return true; + } + } + else if (components.Length == 4) // RGBA + { + if (InternalTryParseByte(components[0], out byte red) && + InternalTryParseByte(components[1], out byte green) && + InternalTryParseByte(components[2], out byte blue) && + InternalTryParseDouble(components[3], out double alpha)) + { + color = new Color((byte)Math.Round(alpha * 255.0), red, green, blue); + return true; + } + } + + // Local function to specially parse a byte value with an optional percentage sign + bool InternalTryParseByte(string inString, out byte outByte) + { + // The percent sign, if it exists, must be at the end of the number + int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); + + if (percentIndex >= 0) + { + var result = double.TryParse( + inString.Substring(0, percentIndex), + NumberStyles.Number, + CultureInfo.InvariantCulture, + out double percentage); + + outByte = (byte)Math.Round((percentage / 100.0) * 255.0); + return result; + } + else + { + return byte.TryParse( + inString, + NumberStyles.Number, + CultureInfo.InvariantCulture, + out outByte); + } + } + + // Local function to specially parse a double value with an optional percentage sign + bool InternalTryParseDouble(string inString, out double outDouble) + { + // The percent sign, if it exists, must be at the end of the number + int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); + + if (percentIndex >= 0) + { + var result = double.TryParse( + inString.Substring(0, percentIndex), + NumberStyles.Number, + CultureInfo.InvariantCulture, + out double percentage); + + outDouble = percentage / 100.0; + return result; + } + else + { + return double.TryParse( + inString, + NumberStyles.Number, + CultureInfo.InvariantCulture, + out outDouble); + } + } + + return false; + } + + /// + /// Returns the string representation of the color. + /// + /// + /// The string representation of the color. + /// + public override string ToString() + { + uint rgb = ToUint32(); + return KnownColors.GetKnownColorName(rgb) ?? $"#{rgb.ToString("x8", CultureInfo.InvariantCulture)}"; + } + + /// + /// Returns the integer representation of the color. + /// + /// + /// The integer representation of the color. + /// + public uint ToUint32() + { + return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B; + } + + /// + /// Returns the HSL color model equivalent of this RGB color. + /// + /// The HSL equivalent color. + public HslColor ToHsl() + { + // Don't use the HslColor(Color) constructor to avoid an extra HslColor + return Color.ToHsl(R, G, B, A); + } + + /// + /// Returns the HSV color model equivalent of this RGB color. + /// + /// The HSV equivalent color. + public HsvColor ToHsv() + { + // Don't use the HsvColor(Color) constructor to avoid an extra HsvColor + return Color.ToHsv(R, G, B, A); + } + + /// + public bool Equals(Color other) + { + return A == other.A && R == other.R && G == other.G && B == other.B; + } + + /// + public override bool Equals(object? obj) + { + return obj is Color other && Equals(other); + } + + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = A.GetHashCode(); + hashCode = (hashCode * 397) ^ R.GetHashCode(); + hashCode = (hashCode * 397) ^ G.GetHashCode(); + hashCode = (hashCode * 397) ^ B.GetHashCode(); + return hashCode; + } + } + + /// + /// Converts the given RGB color to its HSL color equivalent. + /// + /// The color in the RGB color model. + /// A new equivalent to the given RGBA values. + public static HslColor ToHsl(Color color) + { + // Normalize RGBA components into the 0..1 range + return Color.ToHsl( + (byteToDouble * color.R), + (byteToDouble * color.G), + (byteToDouble * color.B), + (byteToDouble * color.A)); + } + + /// + /// Converts the given RGBA color component values to their HSL color equivalent. + /// + /// The Red component in the RGB color model. + /// The Green component in the RGB color model. + /// The Blue component in the RGB color model. + /// The Alpha component. + /// A new equivalent to the given RGBA values. + public static HslColor ToHsl( + byte red, + byte green, + byte blue, + byte alpha = 0xFF) + { + // Normalize RGBA components into the 0..1 range + return Color.ToHsl( + (byteToDouble * red), + (byteToDouble * green), + (byteToDouble * blue), + (byteToDouble * alpha)); + } + + /// + /// Converts the given RGBA color component values to their HSL color equivalent. + /// + /// + /// Warning: No bounds checks or clamping is done on the input component values. + /// This method is for internal-use only and the caller must ensure bounds. + /// + /// The Red component in the RGB color model within the range 0..1. + /// The Green component in the RGB color model within the range 0..1. + /// The Blue component in the RGB color model within the range 0..1. + /// The Alpha component in the RGB color model within the range 0..1. + /// A new equivalent to the given RGBA values. + internal static HslColor ToHsl( + double r, + double g, + double b, + double a = 1.0) + { + // Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT) + // https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs + // It has been modified. + + double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b); + double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b); + double chroma = max - min; + double h1; + + if (chroma == 0) + { + h1 = 0; + } + else if (max == r) + { + // The % operator doesn't do proper modulo on negative + // numbers, so we'll add 6 before using it + h1 = (((g - b) / chroma) + 6) % 6; + } + else if (max == g) + { + h1 = 2 + ((b - r) / chroma); + } + else + { + h1 = 4 + ((r - g) / chroma); + } + + double lightness = 0.5 * (max + min); + double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1)); + + return new HslColor(a, 60 * h1, saturation, lightness, clampValues: false); + } + + /// + /// Converts the given RGB color to its HSV color equivalent. + /// + /// The color in the RGB color model. + /// A new equivalent to the given RGBA values. + public static HsvColor ToHsv(Color color) + { + // Normalize RGBA components into the 0..1 range + return Color.ToHsv( + (byteToDouble * color.R), + (byteToDouble * color.G), + (byteToDouble * color.B), + (byteToDouble * color.A)); + } + + /// + /// Converts the given RGBA color component values to their HSV color equivalent. + /// + /// The Red component in the RGB color model. + /// The Green component in the RGB color model. + /// The Blue component in the RGB color model. + /// The Alpha component. + /// A new equivalent to the given RGBA values. + public static HsvColor ToHsv( + byte red, + byte green, + byte blue, + byte alpha = 0xFF) + { + // Normalize RGBA components into the 0..1 range + return Color.ToHsv( + (byteToDouble * red), + (byteToDouble * green), + (byteToDouble * blue), + (byteToDouble * alpha)); + } + + /// + /// Converts the given RGBA color component values to their HSV color equivalent. + /// + /// + /// Warning: No bounds checks or clamping is done on the input component values. + /// This method is for internal-use only and the caller must ensure bounds. + /// + /// The Red component in the RGB color model within the range 0..1. + /// The Green component in the RGB color model within the range 0..1. + /// The Blue component in the RGB color model within the range 0..1. + /// The Alpha component in the RGB color model within the range 0..1. + /// A new equivalent to the given RGBA values. + internal static HsvColor ToHsv( + double r, + double g, + double b, + double a = 1.0) + { + // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT) + // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp + // This was used because it is the best documented and likely most optimized for performance + // Alpha support was added + + double hue; + double saturation; + double value; + + double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b); + double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b); + + // The value, a number between 0 and 1, is the largest of R, G, and B (divided by 255). + // Conceptually speaking, it represents how much color is present. + // If at least one of R, G, B is 255, then there exists as much color as there can be. + // If RGB = (0, 0, 0), then there exists no color at all - a value of zero corresponds + // to black (i.e., the absence of any color). + value = max; + + // The "chroma" of the color is a value directly proportional to the extent to which + // the color diverges from greyscale. If, for example, we have RGB = (255, 255, 0), + // then the chroma is maximized - this is a pure yellow, no gray of any kind. + // On the other hand, if we have RGB = (128, 128, 128), then the chroma being zero + // implies that this color is pure greyscale, with no actual hue to be found. + var chroma = max - min; + + // If the chrome is zero, then hue is technically undefined - a greyscale color + // has no hue. For the sake of convenience, we'll just set hue to zero, since + // it will be unused in this circumstance. Since the color is purely gray, + // saturation is also equal to zero - you can think of saturation as basically + // a measure of hue intensity, such that no hue at all corresponds to a + // nonexistent intensity. + if (chroma == 0) + { + hue = 0.0; + saturation = 0.0; + } + else + { + // In this block, hue is properly defined, so we'll extract both hue + // and saturation information from the RGB color. + + // Hue can be thought of as a cyclical thing, between 0 degrees and 360 degrees. + // A hue of 0 degrees is red; 120 degrees is green; 240 degrees is blue; and 360 is back to red. + // Every other hue is somewhere between either red and green, green and blue, and blue and red, + // so every other hue can be thought of as an angle on this color wheel. + // These if/else statements determines where on this color wheel our color lies. + if (r == max) + { + // If the red channel is the most pronounced channel, then we exist + // somewhere between (-60, 60) on the color wheel - i.e., the section around 0 degrees + // where red dominates. We figure out where in that section we are exactly + // by considering whether the green or the blue channel is greater - by subtracting green from blue, + // then if green is greater, we'll nudge ourselves closer to 60, whereas if blue is greater, then + // we'll nudge ourselves closer to -60. We then divide by chroma (which will actually make the result larger, + // since chroma is a value between 0 and 1) to normalize the value to ensure that we get the right hue + // even if we're very close to greyscale. + hue = 60 * (g - b) / chroma; + } + else if (g == max) + { + // We do the exact same for the case where the green channel is the most pronounced channel, + // only this time we want to see if we should tilt towards the blue direction or the red direction. + // We add 120 to center our value in the green third of the color wheel. + hue = 120 + (60 * (b - r) / chroma); + } + else // blue == max + { + // And we also do the exact same for the case where the blue channel is the most pronounced channel, + // only this time we want to see if we should tilt towards the red direction or the green direction. + // We add 240 to center our value in the blue third of the color wheel. + hue = 240 + (60 * (r - g) / chroma); + } + + // Since we want to work within the range [0, 360), we'll add 360 to any value less than zero - + // this will bump red values from within -60 to -1 to 300 to 359. The hue is the same at both values. + if (hue < 0.0) + { + hue += 360.0; + } + + // The saturation, our final HSV axis, can be thought of as a value between 0 and 1 indicating how intense our color is. + // To find it, we divide the chroma - the distance between the minimum and the maximum RGB channels - by the maximum channel (i.e., the value). + // This effectively normalizes the chroma - if the maximum is 0.5 and the minimum is 0, the saturation will be (0.5 - 0) / 0.5 = 1, + // meaning that although this color is not as bright as it can be, the dark color is as intense as it possibly could be. + // If, on the other hand, the maximum is 0.5 and the minimum is 0.25, then the saturation will be (0.5 - 0.25) / 0.5 = 0.5, + // meaning that this color is partially washed out. + // A saturation value of 0 corresponds to a greyscale color, one in which the color is *completely* washed out and there is no actual hue. + saturation = chroma / value; + } + + return new HsvColor(a, hue, saturation, value, clampValues: false); + } + + /// + /// Indicates whether the values of two specified objects are equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are equal; otherwise, false. + public static bool operator ==(Color left, Color right) + { + return left.Equals(right); + } + + /// + /// Indicates whether the values of two specified objects are not equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are not equal; otherwise, false. + public static bool operator !=(Color left, Color right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Colors.cs b/src/Avalonia.Base/Media/Colors.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Colors.cs rename to src/Avalonia.Base/Media/Colors.cs diff --git a/src/Avalonia.Visuals/Media/ConicGradientBrush.cs b/src/Avalonia.Base/Media/ConicGradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/ConicGradientBrush.cs rename to src/Avalonia.Base/Media/ConicGradientBrush.cs diff --git a/src/Avalonia.Base/Media/DashStyle.cs b/src/Avalonia.Base/Media/DashStyle.cs new file mode 100644 index 0000000000..abee580020 --- /dev/null +++ b/src/Avalonia.Base/Media/DashStyle.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Animation; +using Avalonia.Collections; +using Avalonia.Media.Immutable; + +#nullable enable + +namespace Avalonia.Media +{ + /// + /// Represents the sequence of dashes and gaps that will be applied by a . + /// + public class DashStyle : Animatable, IDashStyle, IAffectsRender + { + /// + /// Defines the property. + /// + public static readonly StyledProperty> DashesProperty = + AvaloniaProperty.Register>(nameof(Dashes)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OffsetProperty = + AvaloniaProperty.Register(nameof(Offset)); + + private static ImmutableDashStyle? s_dash; + private static ImmutableDashStyle? s_dot; + private static ImmutableDashStyle? s_dashDot; + private static ImmutableDashStyle? s_dashDotDot; + + /// + /// Initializes a new instance of the class. + /// + public DashStyle() + : this(null, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The dashes collection. + /// The dash sequence offset. + public DashStyle(IEnumerable? dashes, double offset) + { + Dashes = (dashes as AvaloniaList) ?? new AvaloniaList(dashes ?? Array.Empty()); + Offset = offset; + } + + static DashStyle() + { + void RaiseInvalidated(AvaloniaPropertyChangedEventArgs e) + { + ((DashStyle)e.Sender).Invalidated?.Invoke(e.Sender, EventArgs.Empty); + } + + DashesProperty.Changed.Subscribe(RaiseInvalidated); + OffsetProperty.Changed.Subscribe(RaiseInvalidated); + } + + /// + /// Represents a dashed . + /// + public static IDashStyle Dash => s_dash ??= new ImmutableDashStyle(new double[] { 2, 2 }, 1); + + /// + /// Represents a dotted . + /// + public static IDashStyle Dot => s_dot ??= new ImmutableDashStyle(new double[] { 0, 2 }, 0); + + /// + /// Represents a dashed dotted . + /// + public static IDashStyle DashDot => s_dashDot ??= new ImmutableDashStyle(new double[] { 2, 2, 0, 2 }, 1); + + /// + /// Represents a dashed double dotted . + /// + public static IDashStyle DashDotDot => s_dashDotDot ??= new ImmutableDashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1); + + /// + /// Gets or sets the length of alternating dashes and gaps. + /// + public AvaloniaList Dashes + { + get => GetValue(DashesProperty); + set => SetValue(DashesProperty, value); + } + + /// + /// Gets or sets how far in the dash sequence the stroke will start. + /// + public double Offset + { + get => GetValue(OffsetProperty); + set => SetValue(OffsetProperty, value); + } + + IReadOnlyList IDashStyle.Dashes => Dashes; + + /// + /// Raised when the dash style changes. + /// + public event EventHandler? Invalidated; + + /// + /// Returns an immutable clone of the . + /// + /// + public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset); + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DashesProperty) + { + var (oldValue, newValue) = change.GetOldAndNewValue>(); + + if (oldValue is object) + { + oldValue.CollectionChanged -= DashesChanged; + } + + if (newValue is object) + { + newValue.CollectionChanged += DashesChanged; + } + } + } + + private void DashesChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + Invalidated?.Invoke(this, e); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Drawing.cs b/src/Avalonia.Base/Media/Drawing.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Drawing.cs rename to src/Avalonia.Base/Media/Drawing.cs diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs new file mode 100644 index 0000000000..d0f3b9e21a --- /dev/null +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; +using Avalonia.Utilities; +using Avalonia.Media.Imaging; + +namespace Avalonia.Media +{ + public sealed class DrawingContext : IDisposable + { + private readonly bool _ownsImpl; + private int _currentLevel; + + + private static ThreadSafeObjectPool> StateStackPool { get; } = + ThreadSafeObjectPool>.Default; + + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; + + private Stack? _states = StateStackPool.Get(); + + private Stack? _transformContainers = TransformStackPool.Get(); + + readonly struct TransformContainer + { + public readonly Matrix LocalTransform; + public readonly Matrix ContainerTransform; + + public TransformContainer(Matrix localTransform, Matrix containerTransform) + { + LocalTransform = localTransform; + ContainerTransform = containerTransform; + } + } + + public DrawingContext(IDrawingContextImpl impl) + { + PlatformImpl = impl; + _ownsImpl = true; + } + + public DrawingContext(IDrawingContextImpl impl, bool ownsImpl) + { + _ownsImpl = ownsImpl; + PlatformImpl = impl; + } + + public IDrawingContextImpl PlatformImpl { get; } + + private Matrix _currentTransform = Matrix.Identity; + + private Matrix _currentContainerTransform = Matrix.Identity; + + /// + /// Gets the current transform of the drawing context. + /// + public Matrix CurrentTransform + { + get { return _currentTransform; } + private set + { + _currentTransform = value; + var transform = _currentTransform * _currentContainerTransform; + PlatformImpl.Transform = transform; + } + } + + //HACK: This is a temporary hack that is used in the render loop + //to update TransformedBounds property + [Obsolete("HACK for render loop, don't use")] + public Matrix CurrentContainerTransform => _currentContainerTransform; + + /// + /// Draws an image. + /// + /// The image. + /// The rect in the output to draw to. + public void DrawImage(IImage source, Rect rect) + { + _ = source ?? throw new ArgumentNullException(nameof(source)); + + DrawImage(source, new Rect(source.Size), rect); + } + + /// + /// Draws an image. + /// + /// The image. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) + { + _ = source ?? throw new ArgumentNullException(nameof(source)); + + source.Draw(this, sourceRect, destRect, bitmapInterpolationMode); + } + + /// + /// Draws a line. + /// + /// The stroke pen. + /// The first point of the line. + /// The second point of the line. + public void DrawLine(IPen pen, Point p1, Point p2) + { + if (PenIsVisible(pen)) + { + PlatformImpl.DrawLine(pen, p1, p2); + } + } + + /// + /// Draws a geometry. + /// + /// The fill brush. + /// The stroke pen. + /// The geometry. + public void DrawGeometry(IBrush? brush, IPen? pen, Geometry geometry) + { + if (geometry.PlatformImpl is not null) + DrawGeometry(brush, pen, geometry.PlatformImpl); + } + + /// + /// Draws a geometry. + /// + /// The fill brush. + /// The stroke pen. + /// The geometry. + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + _ = geometry ?? throw new ArgumentNullException(nameof(geometry)); + + if (brush != null || PenIsVisible(pen)) + { + PlatformImpl.DrawGeometry(brush, pen, geometry); + } + } + + /// + /// Draws a rectangle with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The rectangle bounds. + /// The radius in the X dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Width/2 + /// + /// The radius in the Y dimension of the rounded corners. + /// This value will be clamped to the range of 0 to Height/2 + /// + /// Box shadow effect parameters + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect, double radiusX = 0, double radiusY = 0, + BoxShadows boxShadows = default) + { + if (brush == null && !PenIsVisible(pen)) + { + return; + } + + if (!MathUtilities.IsZero(radiusX)) + { + radiusX = Math.Min(radiusX, rect.Width / 2); + } + + if (!MathUtilities.IsZero(radiusY)) + { + radiusY = Math.Min(radiusY, rect.Height / 2); + } + + PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); + } + + /// + /// Draws the outline of a rectangle. + /// + /// The pen. + /// The rectangle bounds. + /// The corner radius. + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) + { + DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); + } + + /// + /// Draws an ellipse with the specified Brush and Pen. + /// + /// The brush used to fill the ellipse, or null for no fill. + /// The pen used to stroke the ellipse, or null for no stroke. + /// The location of the center of the ellipse. + /// The horizontal radius of the ellipse. + /// The vertical radius of the ellipse. + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawEllipse(IBrush? brush, IPen? pen, Point center, double radiusX, double radiusY) + { + if (brush == null && !PenIsVisible(pen)) + { + return; + } + + var originX = center.X - radiusX; + var originY = center.Y - radiusY; + var width = radiusX * 2; + var height = radiusY * 2; + + PlatformImpl.DrawEllipse(brush, pen, new Rect(originX, originY, width, height)); + } + + /// + /// Draws a custom drawing operation + /// + /// custom operation + public void Custom(ICustomDrawOperation custom) => PlatformImpl.Custom(custom); + + /// + /// Draws text. + /// + /// The upper-left corner of the text. + /// The text. + public void DrawText(FormattedText text, Point origin) + { + _ = text ?? throw new ArgumentNullException(nameof(text)); + + text.Draw(this, origin); + } + + /// + /// Draws a glyph run. + /// + /// The foreground brush. + /// The glyph run. + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); + + if (foreground != null) + { + PlatformImpl.DrawGlyphRun(foreground, glyphRun); + } + } + + /// + /// Draws a filled rectangle. + /// + /// The brush. + /// The rectangle bounds. + /// The corner radius. + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) + { + DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); + } + + public readonly struct PushedState : IDisposable + { + private readonly int _level; + private readonly DrawingContext _context; + private readonly Matrix _matrix; + private readonly PushedStateType _type; + + public enum PushedStateType + { + None, + Matrix, + Opacity, + Clip, + MatrixContainer, + GeometryClip, + OpacityMask, + } + + public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default(Matrix)) + { + if (context._states is null) + throw new ObjectDisposedException(nameof(DrawingContext)); + + _context = context; + _type = type; + _matrix = matrix; + _level = context._currentLevel += 1; + context._states.Push(this); + } + + public void Dispose() + { + if (_type == PushedStateType.None) + return; + if (_context._states is null || _context._transformContainers is null) + throw new ObjectDisposedException(nameof(DrawingContext)); + if (_context._currentLevel != _level) + throw new InvalidOperationException("Wrong Push/Pop state order"); + _context._currentLevel--; + _context._states.Pop(); + if (_type == PushedStateType.Matrix) + _context.CurrentTransform = _matrix; + else if (_type == PushedStateType.Clip) + _context.PlatformImpl.PopClip(); + else if (_type == PushedStateType.Opacity) + _context.PlatformImpl.PopOpacity(); + else if (_type == PushedStateType.GeometryClip) + _context.PlatformImpl.PopGeometryClip(); + else if (_type == PushedStateType.OpacityMask) + _context.PlatformImpl.PopOpacityMask(); + else if (_type == PushedStateType.MatrixContainer) + { + var cont = _context._transformContainers.Pop(); + _context._currentContainerTransform = cont.ContainerTransform; + _context.CurrentTransform = cont.LocalTransform; + } + } + } + + + public PushedState PushClip(RoundedRect clip) + { + PlatformImpl.PushClip(clip); + return new PushedState(this, PushedState.PushedStateType.Clip); + } + + /// + /// Pushes a clip rectangle. + /// + /// The clip rectangle. + /// A disposable used to undo the clip rectangle. + public PushedState PushClip(Rect clip) + { + PlatformImpl.PushClip(clip); + return new PushedState(this, PushedState.PushedStateType.Clip); + } + + /// + /// Pushes a clip geometry. + /// + /// The clip geometry. + /// A disposable used to undo the clip geometry. + public PushedState PushGeometryClip(Geometry clip) + { + _ = clip ?? throw new ArgumentNullException(nameof(clip)); + + // HACK: This check was added when nullable annotations pointed out that we're potentially + // pushing a null value for the clip here. Ideally we'd return an empty PushedState here but + // I don't want to make that change as part of adding nullable annotations. + if (clip.PlatformImpl is null) + throw new InvalidOperationException("Cannot push empty geometry clip."); + + PlatformImpl.PushGeometryClip(clip.PlatformImpl); + return new PushedState(this, PushedState.PushedStateType.GeometryClip); + } + + /// + /// Pushes an opacity value. + /// + /// The opacity. + /// A disposable used to undo the opacity. + public PushedState PushOpacity(double opacity) + //TODO: Eliminate platform-specific push opacity call + { + PlatformImpl.PushOpacity(opacity); + return new PushedState(this, PushedState.PushedStateType.Opacity); + } + + /// + /// Pushes an opacity mask. + /// + /// The opacity mask. + /// + /// The size of the brush's target area. TODO: Are we sure this is needed? + /// + /// A disposable to undo the opacity mask. + public PushedState PushOpacityMask(IBrush mask, Rect bounds) + { + PlatformImpl.PushOpacityMask(mask, bounds); + return new PushedState(this, PushedState.PushedStateType.OpacityMask); + } + + /// + /// Pushes a matrix post-transformation. + /// + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform * matrix); + + /// + /// Pushes a matrix pre-transformation. + /// + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix * CurrentTransform); + + /// + /// Sets the current matrix transformation. + /// + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushSetTransform(Matrix matrix) + { + var oldMatrix = CurrentTransform; + CurrentTransform = matrix; + + return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); + } + + /// + /// Pushes a new transform context. + /// + /// A disposable used to undo the transformation. + public PushedState PushTransformContainer() + { + if (_transformContainers is null) + throw new ObjectDisposedException(nameof(DrawingContext)); + _transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform)); + _currentContainerTransform = CurrentTransform * _currentContainerTransform; + _currentTransform = Matrix.Identity; + return new PushedState(this, PushedState.PushedStateType.MatrixContainer); + } + + /// + /// Disposes of any resources held by the . + /// + public void Dispose() + { + if (_states is null || _transformContainers is null) + throw new ObjectDisposedException(nameof(DrawingContext)); + while (_states.Count != 0) + _states.Peek().Dispose(); + StateStackPool.Return(_states); + _states = null; + if (_transformContainers.Count != 0) + throw new InvalidOperationException("Transform container stack is non-empty"); + TransformStackPool.Return(_transformContainers); + _transformContainers = null; + if (_ownsImpl) + PlatformImpl.Dispose(); + } + + private static bool PenIsVisible(IPen? pen) + { + return pen?.Brush != null && pen.Thickness > 0; + } + } +} diff --git a/src/Avalonia.Visuals/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs similarity index 100% rename from src/Avalonia.Visuals/Media/DrawingGroup.cs rename to src/Avalonia.Base/Media/DrawingGroup.cs diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs new file mode 100644 index 0000000000..38ddbdfaed --- /dev/null +++ b/src/Avalonia.Base/Media/DrawingImage.cs @@ -0,0 +1,88 @@ +using System; +using Avalonia.Metadata; +using Avalonia.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// An that uses a for content. + /// + public class DrawingImage : AvaloniaObject, IImage, IAffectsRender + { + public DrawingImage() + { + } + + public DrawingImage(Drawing drawing) + { + Drawing = drawing; + } + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + /// + public event EventHandler? Invalidated; + + /// + /// Gets or sets the drawing content. + /// + [Content] + public Drawing Drawing + { + get => GetValue(DrawingProperty); + set => SetValue(DrawingProperty, value); + } + + /// + public Size Size => Drawing?.GetBounds().Size ?? default; + + /// + void IImage.Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + var drawing = Drawing; + + if (drawing == null) + { + return; + } + + var bounds = drawing.GetBounds(); + var scale = Matrix.CreateScale( + destRect.Width / sourceRect.Width, + destRect.Height / sourceRect.Height); + var translate = Matrix.CreateTranslation( + -sourceRect.X + destRect.X - bounds.X, + -sourceRect.Y + destRect.Y - bounds.Y); + + using (context.PushClip(destRect)) + using (context.PushPreTransform(translate * scale)) + { + Drawing?.Draw(context); + } + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DrawingProperty) + { + RaiseInvalidated(EventArgs.Empty); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + } +} diff --git a/src/Avalonia.Visuals/Media/EllipseGeometry.cs b/src/Avalonia.Base/Media/EllipseGeometry.cs similarity index 100% rename from src/Avalonia.Visuals/Media/EllipseGeometry.cs rename to src/Avalonia.Base/Media/EllipseGeometry.cs diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs similarity index 100% rename from src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs rename to src/Avalonia.Base/Media/ExperimentalAcrylicMaterial.cs diff --git a/src/Avalonia.Visuals/Media/FillRule.cs b/src/Avalonia.Base/Media/FillRule.cs similarity index 100% rename from src/Avalonia.Visuals/Media/FillRule.cs rename to src/Avalonia.Base/Media/FillRule.cs diff --git a/src/Avalonia.Visuals/Media/FlowDirection.cs b/src/Avalonia.Base/Media/FlowDirection.cs similarity index 100% rename from src/Avalonia.Visuals/Media/FlowDirection.cs rename to src/Avalonia.Base/Media/FlowDirection.cs diff --git a/src/Avalonia.Visuals/Media/FontFallback.cs b/src/Avalonia.Base/Media/FontFallback.cs similarity index 100% rename from src/Avalonia.Visuals/Media/FontFallback.cs rename to src/Avalonia.Base/Media/FontFallback.cs diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Base/Media/FontFamily.cs similarity index 100% rename from src/Avalonia.Visuals/Media/FontFamily.cs rename to src/Avalonia.Base/Media/FontFamily.cs diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs similarity index 100% rename from src/Avalonia.Visuals/Media/FontManager.cs rename to src/Avalonia.Base/Media/FontManager.cs diff --git a/src/Avalonia.Visuals/Media/FontManagerOptions.cs b/src/Avalonia.Base/Media/FontManagerOptions.cs similarity index 100% rename from src/Avalonia.Visuals/Media/FontManagerOptions.cs rename to src/Avalonia.Base/Media/FontManagerOptions.cs diff --git a/src/Avalonia.Visuals/Media/FontStretch.cs b/src/Avalonia.Base/Media/FontStretch.cs similarity index 100% rename from src/Avalonia.Visuals/Media/FontStretch.cs rename to src/Avalonia.Base/Media/FontStretch.cs diff --git a/src/Avalonia.Visuals/Media/FontStyle.cs b/src/Avalonia.Base/Media/FontStyle.cs similarity index 100% rename from src/Avalonia.Visuals/Media/FontStyle.cs rename to src/Avalonia.Base/Media/FontStyle.cs diff --git a/src/Avalonia.Visuals/Media/FontWeight.cs b/src/Avalonia.Base/Media/FontWeight.cs similarity index 100% rename from src/Avalonia.Visuals/Media/FontWeight.cs rename to src/Avalonia.Base/Media/FontWeight.cs diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs rename to src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs rename to src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs rename to src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs similarity index 100% rename from src/Avalonia.Visuals/Media/FormattedText.cs rename to src/Avalonia.Base/Media/FormattedText.cs diff --git a/src/Avalonia.Visuals/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Geometry.cs rename to src/Avalonia.Base/Media/Geometry.cs diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs similarity index 100% rename from src/Avalonia.Visuals/Media/GeometryDrawing.cs rename to src/Avalonia.Base/Media/GeometryDrawing.cs diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs similarity index 100% rename from src/Avalonia.Visuals/Media/GlyphRun.cs rename to src/Avalonia.Base/Media/GlyphRun.cs diff --git a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs similarity index 100% rename from src/Avalonia.Visuals/Media/GlyphRunDrawing.cs rename to src/Avalonia.Base/Media/GlyphRunDrawing.cs diff --git a/src/Avalonia.Visuals/Media/GlyphRunMetrics.cs b/src/Avalonia.Base/Media/GlyphRunMetrics.cs similarity index 100% rename from src/Avalonia.Visuals/Media/GlyphRunMetrics.cs rename to src/Avalonia.Base/Media/GlyphRunMetrics.cs diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Base/Media/GlyphTypeface.cs similarity index 100% rename from src/Avalonia.Visuals/Media/GlyphTypeface.cs rename to src/Avalonia.Base/Media/GlyphTypeface.cs diff --git a/src/Avalonia.Visuals/Media/GradientBrush.cs b/src/Avalonia.Base/Media/GradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/GradientBrush.cs rename to src/Avalonia.Base/Media/GradientBrush.cs diff --git a/src/Avalonia.Visuals/Media/GradientSpreadMethod.cs b/src/Avalonia.Base/Media/GradientSpreadMethod.cs similarity index 100% rename from src/Avalonia.Visuals/Media/GradientSpreadMethod.cs rename to src/Avalonia.Base/Media/GradientSpreadMethod.cs diff --git a/src/Avalonia.Visuals/Media/GradientStop.cs b/src/Avalonia.Base/Media/GradientStop.cs similarity index 100% rename from src/Avalonia.Visuals/Media/GradientStop.cs rename to src/Avalonia.Base/Media/GradientStop.cs diff --git a/src/Avalonia.Visuals/Media/GradientStops.cs b/src/Avalonia.Base/Media/GradientStops.cs similarity index 100% rename from src/Avalonia.Visuals/Media/GradientStops.cs rename to src/Avalonia.Base/Media/GradientStops.cs diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs new file mode 100644 index 0000000000..e8a4d6f94f --- /dev/null +++ b/src/Avalonia.Base/Media/HslColor.cs @@ -0,0 +1,519 @@ +// Color conversion portions of this source file are adapted from the Windows Community Toolkit project. +// (https://github.com/CommunityToolkit/WindowsCommunityToolkit) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Globalization; +using System.Text; +using Avalonia.Utilities; + +namespace Avalonia.Media +{ + /// + /// Defines a color using the hue/saturation/lightness (HSL) model. + /// This uses a cylindrical-coordinate representation of a color. + /// +#if !BUILDTASK + public +#endif + readonly struct HslColor : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The Alpha (transparency) component in the range from 0..1. + /// The Hue component in the range from 0..360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// The Saturation component in the range from 0..1. + /// The Lightness component in the range from 0..1. + public HslColor( + double alpha, + double hue, + double saturation, + double lightness) + { + A = MathUtilities.Clamp(alpha, 0.0, 1.0); + H = MathUtilities.Clamp(hue, 0.0, 360.0); + S = MathUtilities.Clamp(saturation, 0.0, 1.0); + L = MathUtilities.Clamp(lightness, 0.0, 1.0); + + // The maximum value of Hue is technically 360 minus epsilon (just below 360). + // This is because, in a color circle, 360 degrees is equivalent to 0 degrees. + // However, that is too tricky to work with in code and isn't as intuitive. + // Therefore, since 360 == 0, just wrap 360 if needed back to 0. + H = (H == 360.0 ? 0 : H); + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// This constructor exists only for internal use where performance is critical. + /// Whether or not the component values are in the correct ranges must be known. + /// + /// The Alpha (transparency) component in the range from 0..1. + /// The Hue component in the range from 0..360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// The Saturation component in the range from 0..1. + /// The Lightness component in the range from 0..1. + /// Whether to clamp component values to their required ranges. + internal HslColor( + double alpha, + double hue, + double saturation, + double lightness, + bool clampValues) + { + if (clampValues) + { + A = MathUtilities.Clamp(alpha, 0.0, 1.0); + H = MathUtilities.Clamp(hue, 0.0, 360.0); + S = MathUtilities.Clamp(saturation, 0.0, 1.0); + L = MathUtilities.Clamp(lightness, 0.0, 1.0); + + // See comments in constructor above + H = (H == 360.0 ? 0 : H); + } + else + { + A = alpha; + H = hue; + S = saturation; + L = lightness; + } + } + + /// + /// Initializes a new instance of the struct. + /// + /// The RGB color to convert to HSL. + public HslColor(Color color) + { + var hsl = Color.ToHsl(color); + + A = hsl.A; + H = hsl.H; + S = hsl.S; + L = hsl.L; + } + + /// + /// Gets the Alpha (transparency) component in the range from 0..1 (percentage). + /// + /// + /// + /// 0 is fully transparent. + /// 1 is fully opaque. + /// + /// + public double A { get; } + + /// + /// Gets the Hue component in the range from 0..360 (degrees). + /// This is the color's location, in degrees, on a color wheel/circle from 0 to 360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// + /// + /// + /// 0/360 degrees is Red. + /// 60 degrees is Yellow. + /// 120 degrees is Green. + /// 180 degrees is Cyan. + /// 240 degrees is Blue. + /// 300 degrees is Magenta. + /// + /// + public double H { get; } + + /// + /// Gets the Saturation component in the range from 0..1 (percentage). + /// + /// + /// + /// 0 is a shade of gray (no color). + /// 1 is the full color. + /// + /// + public double S { get; } + + /// + /// Gets the Lightness component in the range from 0..1 (percentage). + /// + /// + /// + /// 0 is fully black. + /// 1 is fully white. + /// + /// + public double L { get; } + + /// + public bool Equals(HslColor other) + { + return other.A == A && + other.H == H && + other.S == S && + other.L == L; + } + + /// + public override bool Equals(object? obj) + { + if (obj is HslColor hslColor) + { + return Equals(hslColor); + } + else + { + return false; + } + } + + /// + /// Gets a hashcode for this object. + /// Hashcode is not guaranteed to be unique. + /// + /// The hashcode for this object. + public override int GetHashCode() + { + // Same algorithm as Color + // This is used instead of HashCode.Combine() due to .NET Standard 2.0 requirements + unchecked + { + int hashCode = A.GetHashCode(); + hashCode = (hashCode * 397) ^ H.GetHashCode(); + hashCode = (hashCode * 397) ^ S.GetHashCode(); + hashCode = (hashCode * 397) ^ L.GetHashCode(); + return hashCode; + } + } + + /// + /// Returns the RGB color model equivalent of this HSL color. + /// + /// The RGB equivalent color. + public Color ToRgb() + { + // Use the by-component conversion method directly for performance + return HslColor.ToRgb(H, S, L, A); + } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + + // Use a format similar to CSS. However: + // - To ensure precision is never lost, allow decimal places. + // This is especially important for round-trip serialization. + // - To maintain numerical consistency, do not use percent. + // + // Example: + // + // hsla(hue, saturation, lightness, alpha) + // hsla(230, 1.0, 0.5, 1.0) + // + + sb.Append("hsva("); + sb.Append(H.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(S.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(L.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(A.ToString(CultureInfo.InvariantCulture)); + sb.Append(')'); + + return sb.ToString(); + } + + /// + /// Parses an HSL color string. + /// + /// The HSL color string to parse. + /// The parsed . + public static HslColor Parse(string s) + { + if (s is null) + { + throw new ArgumentNullException(nameof(s)); + } + + if (TryParse(s, out HslColor hslColor)) + { + return hslColor; + } + + throw new FormatException($"Invalid HSL color string: '{s}'."); + } + + /// + /// Parses an HSL color string. + /// + /// The HSL color string to parse. + /// The parsed . + /// True if parsing was successful; otherwise, false. + public static bool TryParse(string s, out HslColor hslColor) + { + bool prefixMatched = false; + + hslColor = default; + + if (s is null) + { + return false; + } + + string workingString = s.Trim(); + + if (workingString.Length == 0 || + workingString.IndexOf(",", StringComparison.Ordinal) < 0) + { + return false; + } + + // Note: The length checks are also an important optimization. + // The shortest possible format is "hsl(0,0,0)", Length = 10. + + if (workingString.Length >= 11 && + workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) && + workingString.EndsWith(")", StringComparison.Ordinal)) + { + workingString = workingString.Substring(5, workingString.Length - 6); + prefixMatched = true; + } + + if (prefixMatched == false && + workingString.Length >= 10 && + workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) && + workingString.EndsWith(")", StringComparison.Ordinal)) + { + workingString = workingString.Substring(4, workingString.Length - 5); + prefixMatched = true; + } + + if (prefixMatched == false) + { + return false; + } + + string[] components = workingString.Split(','); + + if (components.Length == 3) // HSL + { + if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && + TryInternalParse(components[1], out double saturation) && + TryInternalParse(components[2], out double lightness)) + { + hslColor = new HslColor(1.0, hue, saturation, lightness); + return true; + } + } + else if (components.Length == 4) // HSLA + { + if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && + TryInternalParse(components[1], out double saturation) && + TryInternalParse(components[2], out double lightness) && + TryInternalParse(components[3], out double alpha)) + { + hslColor = new HslColor(alpha, hue, saturation, lightness); + return true; + } + } + + // Local function to specially parse a double value with an optional percentage sign + bool TryInternalParse(string inString, out double outDouble) + { + // The percent sign, if it exists, must be at the end of the number + int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); + + if (percentIndex >= 0) + { + var result = double.TryParse( + inString.Substring(0, percentIndex), + NumberStyles.Number, + CultureInfo.InvariantCulture, + out double percentage); + + outDouble = percentage / 100.0; + return result; + } + else + { + return double.TryParse( + inString, + NumberStyles.Number, + CultureInfo.InvariantCulture, + out outDouble); + } + } + + return false; + } + + /// + /// Creates a new from individual color component values. + /// + /// + /// This exists for symmetry with the struct; however, the + /// appropriate constructor should commonly be used instead. + /// + /// The Alpha (transparency) component in the range from 0..1. + /// The Hue component in the range from 0..360. + /// The Saturation component in the range from 0..1. + /// The Lightness component in the range from 0..1. + /// A new built from the individual color component values. + public static HslColor FromAhsl(double a, double h, double s, double l) + { + return new HslColor(a, h, s, l); + } + + /// + /// Creates a new from individual color component values. + /// + /// + /// This exists for symmetry with the struct; however, the + /// appropriate constructor should commonly be used instead. + /// + /// The Hue component in the range from 0..360. + /// The Saturation component in the range from 0..1. + /// The Lightness component in the range from 0..1. + /// A new built from the individual color component values. + public static HslColor FromHsl(double h, double s, double l) + { + return new HslColor(1.0, h, s, l); + } + + /// + /// Converts the given HSL color to its RGB color equivalent. + /// + /// The color in the HSL color model. + /// A new RGB equivalent to the given HSLA values. + public static Color ToRgb(HslColor hslColor) + { + return HslColor.ToRgb(hslColor.H, hslColor.S, hslColor.L, hslColor.A); + } + + /// + /// Converts the given HSLA color component values to their RGB color equivalent. + /// + /// The Hue component in the HSL color model in the range from 0..360. + /// The Saturation component in the HSL color model in the range from 0..1. + /// The Lightness component in the HSL color model in the range from 0..1. + /// The Alpha component in the range from 0..1. + /// A new RGB equivalent to the given HSLA values. + public static Color ToRgb( + double hue, + double saturation, + double lightness, + double alpha = 1.0) + { + // Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT) + // https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs + // It has been modified to ensure input ranges and not throw exceptions. + + // We want the hue to be between 0 and 359, + // so we first ensure that that's the case. + while (hue >= 360.0) + { + hue -= 360.0; + } + + while (hue < 0.0) + { + hue += 360.0; + } + + // We similarly clamp saturation, lightness and alpha between 0 and 1. + saturation = saturation < 0.0 ? 0.0 : saturation; + saturation = saturation > 1.0 ? 1.0 : saturation; + + lightness = lightness < 0.0 ? 0.0 : lightness; + lightness = lightness > 1.0 ? 1.0 : lightness; + + alpha = alpha < 0.0 ? 0.0 : alpha; + alpha = alpha > 1.0 ? 1.0 : alpha; + + double chroma = (1 - Math.Abs((2 * lightness) - 1)) * saturation; + double h1 = hue / 60; + double x = chroma * (1 - Math.Abs((h1 % 2) - 1)); + double m = lightness - (0.5 * chroma); + double r1, g1, b1; + + if (h1 < 1) + { + r1 = chroma; + g1 = x; + b1 = 0; + } + else if (h1 < 2) + { + r1 = x; + g1 = chroma; + b1 = 0; + } + else if (h1 < 3) + { + r1 = 0; + g1 = chroma; + b1 = x; + } + else if (h1 < 4) + { + r1 = 0; + g1 = x; + b1 = chroma; + } + else if (h1 < 5) + { + r1 = x; + g1 = 0; + b1 = chroma; + } + else + { + r1 = chroma; + g1 = 0; + b1 = x; + } + + return Color.FromArgb( + (byte)Math.Round(255 * alpha), + (byte)Math.Round(255 * (r1 + m)), + (byte)Math.Round(255 * (g1 + m)), + (byte)Math.Round(255 * (b1 + m))); + } + + /// + /// Indicates whether the values of two specified objects are equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are equal; otherwise, false. + public static bool operator ==(HslColor left, HslColor right) + { + return left.Equals(right); + } + + /// + /// Indicates whether the values of two specified objects are not equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are not equal; otherwise, false. + public static bool operator !=(HslColor left, HslColor right) + { + return !(left == right); + } + + /// + /// Explicit conversion from an to a . + /// + /// The to convert. + public static explicit operator Color(HslColor hslColor) + { + return hslColor.ToRgb(); + } + } +} diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs new file mode 100644 index 0000000000..924ef4778b --- /dev/null +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -0,0 +1,567 @@ +// Color conversion portions of this source file are adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; +using System.Globalization; +using System.Text; +using Avalonia.Utilities; + +namespace Avalonia.Media +{ + /// + /// Defines a color using the hue/saturation/value (HSV) model. + /// This uses a cylindrical-coordinate representation of a color. + /// +#if !BUILDTASK + public +#endif + readonly struct HsvColor : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The Alpha (transparency) component in the range from 0..1. + /// The Hue component in the range from 0..360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// The Saturation component in the range from 0..1. + /// The Value component in the range from 0..1. + public HsvColor( + double alpha, + double hue, + double saturation, + double value) + { + A = MathUtilities.Clamp(alpha, 0.0, 1.0); + H = MathUtilities.Clamp(hue, 0.0, 360.0); + S = MathUtilities.Clamp(saturation, 0.0, 1.0); + V = MathUtilities.Clamp(value, 0.0, 1.0); + + // The maximum value of Hue is technically 360 minus epsilon (just below 360). + // This is because, in a color circle, 360 degrees is equivalent to 0 degrees. + // However, that is too tricky to work with in code and isn't as intuitive. + // Therefore, since 360 == 0, just wrap 360 if needed back to 0. + H = (H == 360.0 ? 0 : H); + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// This constructor exists only for internal use where performance is critical. + /// Whether or not the component values are in the correct ranges must be known. + /// + /// The Alpha (transparency) component in the range from 0..1. + /// The Hue component in the range from 0..360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// The Saturation component in the range from 0..1. + /// The Value component in the range from 0..1. + /// Whether to clamp component values to their required ranges. + internal HsvColor( + double alpha, + double hue, + double saturation, + double value, + bool clampValues) + { + if (clampValues) + { + A = MathUtilities.Clamp(alpha, 0.0, 1.0); + H = MathUtilities.Clamp(hue, 0.0, 360.0); + S = MathUtilities.Clamp(saturation, 0.0, 1.0); + V = MathUtilities.Clamp(value, 0.0, 1.0); + + // See comments in constructor above + H = (H == 360.0 ? 0 : H); + } + else + { + A = alpha; + H = hue; + S = saturation; + V = value; + } + } + + /// + /// Initializes a new instance of the struct. + /// + /// The RGB color to convert to HSV. + public HsvColor(Color color) + { + var hsv = Color.ToHsv(color); + + A = hsv.A; + H = hsv.H; + S = hsv.S; + V = hsv.V; + } + + /// + /// Gets the Alpha (transparency) component in the range from 0..1 (percentage). + /// + /// + /// + /// 0 is fully transparent. + /// 1 is fully opaque. + /// + /// + public double A { get; } + + /// + /// Gets the Hue component in the range from 0..360 (degrees). + /// This is the color's location, in degrees, on a color wheel/circle from 0 to 360. + /// Note that 360 is equivalent to 0 and will be adjusted automatically. + /// + /// + /// + /// 0/360 degrees is Red. + /// 60 degrees is Yellow. + /// 120 degrees is Green. + /// 180 degrees is Cyan. + /// 240 degrees is Blue. + /// 300 degrees is Magenta. + /// + /// + public double H { get; } + + /// + /// Gets the Saturation component in the range from 0..1 (percentage). + /// + /// + /// + /// 0 is a shade of gray (no color). + /// 1 is the full color. + /// + /// + public double S { get; } + + /// + /// Gets the Value (or Brightness/Intensity) component in the range from 0..1 (percentage). + /// + /// + /// + /// 0 is fully black and shows no color. + /// 1 is the brightest and shows full color. + /// + /// + public double V { get; } + + /// + public bool Equals(HsvColor other) + { + return other.A == A && + other.H == H && + other.S == S && + other.V == V; + } + + /// + public override bool Equals(object? obj) + { + if (obj is HsvColor hsvColor) + { + return Equals(hsvColor); + } + else + { + return false; + } + } + + /// + /// Gets a hashcode for this object. + /// Hashcode is not guaranteed to be unique. + /// + /// The hashcode for this object. + public override int GetHashCode() + { + // Same algorithm as Color + // This is used instead of HashCode.Combine() due to .NET Standard 2.0 requirements + unchecked + { + int hashCode = A.GetHashCode(); + hashCode = (hashCode * 397) ^ H.GetHashCode(); + hashCode = (hashCode * 397) ^ S.GetHashCode(); + hashCode = (hashCode * 397) ^ V.GetHashCode(); + return hashCode; + } + } + + /// + /// Returns the RGB color model equivalent of this HSV color. + /// + /// The RGB equivalent color. + public Color ToRgb() + { + // Use the by-component conversion method directly for performance + return HsvColor.ToRgb(H, S, V, A); + } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + + // Use a format similar to CSS. However: + // - To ensure precision is never lost, allow decimal places. + // This is especially important for round-trip serialization. + // - To maintain numerical consistency, do not use percent. + // + // Example: + // + // hsva(hue, saturation, value, alpha) + // hsva(230, 1.0, 0.5, 1.0) + // + + sb.Append("hsva("); + sb.Append(H.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(S.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(V.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(A.ToString(CultureInfo.InvariantCulture)); + sb.Append(')'); + + return sb.ToString(); + } + + /// + /// Parses an HSV color string. + /// + /// The HSV color string to parse. + /// The parsed . + public static HsvColor Parse(string s) + { + if (s is null) + { + throw new ArgumentNullException(nameof(s)); + } + + if (TryParse(s, out HsvColor hsvColor)) + { + return hsvColor; + } + + throw new FormatException($"Invalid HSV color string: '{s}'."); + } + + /// + /// Parses an HSV color string. + /// + /// The HSV color string to parse. + /// The parsed . + /// True if parsing was successful; otherwise, false. + public static bool TryParse(string s, out HsvColor hsvColor) + { + bool prefixMatched = false; + + hsvColor = default; + + if (s is null) + { + return false; + } + + string workingString = s.Trim(); + + if (workingString.Length == 0 || + workingString.IndexOf(",", StringComparison.Ordinal) < 0) + { + return false; + } + + // Note: The length checks are also an important optimization. + // The shortest possible format is "hsv(0,0,0)", Length = 10. + + if (workingString.Length >= 11 && + workingString.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) && + workingString.EndsWith(")", StringComparison.Ordinal)) + { + workingString = workingString.Substring(5, workingString.Length - 6); + prefixMatched = true; + } + + if (prefixMatched == false && + workingString.Length >= 10 && + workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) && + workingString.EndsWith(")", StringComparison.Ordinal)) + { + workingString = workingString.Substring(4, workingString.Length - 5); + prefixMatched = true; + } + + if (prefixMatched == false) + { + return false; + } + + string[] components = workingString.Split(','); + + if (components.Length == 3) // HSV + { + if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && + TryInternalParse(components[1], out double saturation) && + TryInternalParse(components[2], out double value)) + { + hsvColor = new HsvColor(1.0, hue, saturation, value); + return true; + } + } + else if (components.Length == 4) // HSVA + { + if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && + TryInternalParse(components[1], out double saturation) && + TryInternalParse(components[2], out double value) && + TryInternalParse(components[3], out double alpha)) + { + hsvColor = new HsvColor(alpha, hue, saturation, value); + return true; + } + } + + // Local function to specially parse a double value with an optional percentage sign + bool TryInternalParse(string inString, out double outDouble) + { + // The percent sign, if it exists, must be at the end of the number + int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); + + if (percentIndex >= 0) + { + var result = double.TryParse( + inString.Substring(0, percentIndex), + NumberStyles.Number, + CultureInfo.InvariantCulture, + out double percentage); + + outDouble = percentage / 100.0; + return result; + } + else + { + return double.TryParse( + inString, + NumberStyles.Number, + CultureInfo.InvariantCulture, + out outDouble); + } + } + + return false; + } + + /// + /// Creates a new from individual color component values. + /// + /// + /// This exists for symmetry with the struct; however, the + /// appropriate constructor should commonly be used instead. + /// + /// The Alpha (transparency) component in the range from 0..1. + /// The Hue component in the range from 0..360. + /// The Saturation component in the range from 0..1. + /// The Value component in the range from 0..1. + /// A new built from the individual color component values. + public static HsvColor FromAhsv(double a, double h, double s, double v) + { + return new HsvColor(a, h, s, v); + } + + /// + /// Creates a new from individual color component values. + /// + /// + /// This exists for symmetry with the struct; however, the + /// appropriate constructor should commonly be used instead. + /// + /// The Hue component in the range from 0..360. + /// The Saturation component in the range from 0..1. + /// The Value component in the range from 0..1. + /// A new built from the individual color component values. + public static HsvColor FromHsv(double h, double s, double v) + { + return new HsvColor(1.0, h, s, v); + } + + /// + /// Converts the given HSV color to its RGB color equivalent. + /// + /// The color in the HSV color model. + /// A new RGB equivalent to the given HSVA values. + public static Color ToRgb(HsvColor hsvColor) + { + return HsvColor.ToRgb(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A); + } + + /// + /// Converts the given HSVA color component values to their RGB color equivalent. + /// + /// The Hue component in the HSV color model in the range from 0..360. + /// The Saturation component in the HSV color model in the range from 0..1. + /// The Value component in the HSV color model in the range from 0..1. + /// The Alpha component in the range from 0..1. + /// A new RGB equivalent to the given HSVA values. + public static Color ToRgb( + double hue, + double saturation, + double value, + double alpha = 1.0) + { + // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT) + // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp + // This was used because it is the best documented and likely most optimized for performance + // Alpha support was added + + // We want the hue to be between 0 and 359, + // so we first ensure that that's the case. + while (hue >= 360.0) + { + hue -= 360.0; + } + + while (hue < 0.0) + { + hue += 360.0; + } + + // We similarly clamp saturation, value and alpha between 0 and 1. + saturation = saturation < 0.0 ? 0.0 : saturation; + saturation = saturation > 1.0 ? 1.0 : saturation; + + value = value < 0.0 ? 0.0 : value; + value = value > 1.0 ? 1.0 : value; + + alpha = alpha < 0.0 ? 0.0 : alpha; + alpha = alpha > 1.0 ? 1.0 : alpha; + + // The first thing that we need to do is to determine the chroma (see above for its definition). + // Remember from above that: + // + // 1. The chroma is the difference between the maximum and the minimum of the RGB channels, + // 2. The value is the maximum of the RGB channels, and + // 3. The saturation comes from dividing the chroma by the maximum of the RGB channels (i.e., the value). + // + // From these facts, you can see that we can retrieve the chroma by simply multiplying the saturation and the value, + // and we can retrieve the minimum of the RGB channels by subtracting the chroma from the value. + var chroma = saturation * value; + var min = value - chroma; + + // If the chroma is zero, then we have a greyscale color. In that case, the maximum and the minimum RGB channels + // have the same value (and, indeed, all of the RGB channels are the same), so we can just immediately return + // the minimum value as the value of all the channels. + if (chroma == 0) + { + return Color.FromArgb( + (byte)Math.Round(alpha * 255), + (byte)Math.Round(min * 255), + (byte)Math.Round(min * 255), + (byte)Math.Round(min * 255)); + } + + // If the chroma is not zero, then we need to continue. The first step is to figure out + // what section of the color wheel we're located in. In order to do that, we'll divide the hue by 60. + // The resulting value means we're in one of the following locations: + // + // 0 - Between red and yellow. + // 1 - Between yellow and green. + // 2 - Between green and cyan. + // 3 - Between cyan and blue. + // 4 - Between blue and purple. + // 5 - Between purple and red. + // + // In each of these sextants, one of the RGB channels is completely present, one is partially present, and one is not present. + // For example, as we transition between red and yellow, red is completely present, green is becoming increasingly present, and blue is not present. + // Then, as we transition from yellow and green, green is now completely present, red is becoming decreasingly present, and blue is still not present. + // As we transition from green to cyan, green is still completely present, blue is becoming increasingly present, and red is no longer present. And so on. + // + // To convert from hue to RGB value, we first need to figure out which of the three channels is in which configuration + // in the sextant that we're located in. Next, we figure out what value the completely-present color should have. + // We know that chroma = (max - min), and we know that this color is the max color, so to find its value we simply add + // min to chroma to retrieve max. Finally, we consider how far we've transitioned from the pure form of that color + // to the next color (e.g., how far we are from pure red towards yellow), and give a value to the partially present channel + // equal to the minimum plus the chroma (i.e., the max minus the min), multiplied by the percentage towards the new color. + // This gets us a value between the maximum and the minimum representing the partially present channel. + // Finally, the not-present color must be equal to the minimum value, since it is the one least participating in the overall color. + int sextant = (int)(hue / 60); + double intermediateColorPercentage = (hue / 60) - sextant; + double max = chroma + min; + + double r = 0; + double g = 0; + double b = 0; + + switch (sextant) + { + case 0: + r = max; + g = min + (chroma * intermediateColorPercentage); + b = min; + break; + case 1: + r = min + (chroma * (1 - intermediateColorPercentage)); + g = max; + b = min; + break; + case 2: + r = min; + g = max; + b = min + (chroma * intermediateColorPercentage); + break; + case 3: + r = min; + g = min + (chroma * (1 - intermediateColorPercentage)); + b = max; + break; + case 4: + r = min + (chroma * intermediateColorPercentage); + g = min; + b = max; + break; + case 5: + r = max; + g = min; + b = min + (chroma * (1 - intermediateColorPercentage)); + break; + } + + return Color.FromArgb( + (byte)Math.Round(alpha * 255), + (byte)Math.Round(r * 255), + (byte)Math.Round(g * 255), + (byte)Math.Round(b * 255)); + } + + /// + /// Indicates whether the values of two specified objects are equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are equal; otherwise, false. + public static bool operator ==(HsvColor left, HsvColor right) + { + return left.Equals(right); + } + + /// + /// Indicates whether the values of two specified objects are not equal. + /// + /// The first object to compare. + /// The second object to compare. + /// True if left and right are not equal; otherwise, false. + public static bool operator !=(HsvColor left, HsvColor right) + { + return !(left == right); + } + + /// + /// Explicit conversion from an to a . + /// + /// The to convert. + public static explicit operator Color(HsvColor hsvColor) + { + return hsvColor.ToRgb(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/IAffectsRender.cs b/src/Avalonia.Base/Media/IAffectsRender.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IAffectsRender.cs rename to src/Avalonia.Base/Media/IAffectsRender.cs diff --git a/src/Avalonia.Visuals/Media/IBrush.cs b/src/Avalonia.Base/Media/IBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IBrush.cs rename to src/Avalonia.Base/Media/IBrush.cs diff --git a/src/Avalonia.Visuals/Media/IConicGradientBrush.cs b/src/Avalonia.Base/Media/IConicGradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IConicGradientBrush.cs rename to src/Avalonia.Base/Media/IConicGradientBrush.cs diff --git a/src/Avalonia.Visuals/Media/IDashStyle.cs b/src/Avalonia.Base/Media/IDashStyle.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IDashStyle.cs rename to src/Avalonia.Base/Media/IDashStyle.cs diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs b/src/Avalonia.Base/Media/IExperimentalAcrylicMaterial.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs rename to src/Avalonia.Base/Media/IExperimentalAcrylicMaterial.cs diff --git a/src/Avalonia.Visuals/Media/IGradientBrush.cs b/src/Avalonia.Base/Media/IGradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IGradientBrush.cs rename to src/Avalonia.Base/Media/IGradientBrush.cs diff --git a/src/Avalonia.Visuals/Media/IGradientStop.cs b/src/Avalonia.Base/Media/IGradientStop.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IGradientStop.cs rename to src/Avalonia.Base/Media/IGradientStop.cs diff --git a/src/Avalonia.Base/Media/IImage.cs b/src/Avalonia.Base/Media/IImage.cs new file mode 100644 index 0000000000..cbe25b7b58 --- /dev/null +++ b/src/Avalonia.Base/Media/IImage.cs @@ -0,0 +1,28 @@ +using Avalonia.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// Represents a raster or vector image. + /// + public interface IImage + { + /// + /// Gets the size of the image, in device independent pixels. + /// + Size Size { get; } + + /// + /// Draws the image to a . + /// + /// The drawing context. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + void Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode); + } +} diff --git a/src/Avalonia.Visuals/Media/IImageBrush.cs b/src/Avalonia.Base/Media/IImageBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IImageBrush.cs rename to src/Avalonia.Base/Media/IImageBrush.cs diff --git a/src/Avalonia.Visuals/Media/ILinearGradientBrush.cs b/src/Avalonia.Base/Media/ILinearGradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/ILinearGradientBrush.cs rename to src/Avalonia.Base/Media/ILinearGradientBrush.cs diff --git a/src/Avalonia.Visuals/Media/IMutableBrush.cs b/src/Avalonia.Base/Media/IMutableBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IMutableBrush.cs rename to src/Avalonia.Base/Media/IMutableBrush.cs diff --git a/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Base/Media/IMutableExperimentalAcrylicMaterial.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs rename to src/Avalonia.Base/Media/IMutableExperimentalAcrylicMaterial.cs diff --git a/src/Avalonia.Visuals/Media/IMutableTransform.cs b/src/Avalonia.Base/Media/IMutableTransform.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IMutableTransform.cs rename to src/Avalonia.Base/Media/IMutableTransform.cs diff --git a/src/Avalonia.Visuals/Media/IPen.cs b/src/Avalonia.Base/Media/IPen.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IPen.cs rename to src/Avalonia.Base/Media/IPen.cs diff --git a/src/Avalonia.Visuals/Media/IRadialGradientBrush.cs b/src/Avalonia.Base/Media/IRadialGradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IRadialGradientBrush.cs rename to src/Avalonia.Base/Media/IRadialGradientBrush.cs diff --git a/src/Avalonia.Visuals/Media/ISolidColorBrush.cs b/src/Avalonia.Base/Media/ISolidColorBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/ISolidColorBrush.cs rename to src/Avalonia.Base/Media/ISolidColorBrush.cs diff --git a/src/Avalonia.Base/Media/ITileBrush.cs b/src/Avalonia.Base/Media/ITileBrush.cs new file mode 100644 index 0000000000..991857eec9 --- /dev/null +++ b/src/Avalonia.Base/Media/ITileBrush.cs @@ -0,0 +1,49 @@ +using Avalonia.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// A brush which displays a repeating image. + /// + public interface ITileBrush : IBrush + { + /// + /// Gets the horizontal alignment of a tile in the destination. + /// + AlignmentX AlignmentX { get; } + + /// + /// Gets the horizontal alignment of a tile in the destination. + /// + AlignmentY AlignmentY { get; } + + /// + /// Gets the rectangle on the destination in which to paint a tile. + /// + RelativeRect DestinationRect { get; } + + /// + /// Gets the rectangle of the source image that will be displayed. + /// + RelativeRect SourceRect { get; } + + /// + /// Gets a value indicating how the source rectangle will be stretched to fill the + /// destination rect. + /// + Stretch Stretch { get; } + + /// + /// Gets the brush's tile mode. + /// + TileMode TileMode { get; } + + /// + /// Gets the bitmap interpolation mode. + /// + /// + /// The bitmap interpolation mode. + /// + BitmapInterpolationMode BitmapInterpolationMode { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/ITransform.cs b/src/Avalonia.Base/Media/ITransform.cs similarity index 100% rename from src/Avalonia.Visuals/Media/ITransform.cs rename to src/Avalonia.Base/Media/ITransform.cs diff --git a/src/Avalonia.Visuals/Media/IVisualBrush.cs b/src/Avalonia.Base/Media/IVisualBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/IVisualBrush.cs rename to src/Avalonia.Base/Media/IVisualBrush.cs diff --git a/src/Avalonia.Visuals/Media/ImageBrush.cs b/src/Avalonia.Base/Media/ImageBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/ImageBrush.cs rename to src/Avalonia.Base/Media/ImageBrush.cs diff --git a/src/Avalonia.Visuals/Media/ImageDrawing.cs b/src/Avalonia.Base/Media/ImageDrawing.cs similarity index 100% rename from src/Avalonia.Visuals/Media/ImageDrawing.cs rename to src/Avalonia.Base/Media/ImageDrawing.cs diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs new file mode 100644 index 0000000000..5f1617d778 --- /dev/null +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -0,0 +1,174 @@ +using System; +using System.IO; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Media.Imaging +{ + /// + /// Holds a bitmap image. + /// + public class Bitmap : IBitmap + { + /// + /// Loads a Bitmap from a stream and decodes at the desired width. Aspect ratio is maintained. + /// This is more efficient than loading and then resizing. + /// + /// The stream to read the bitmap from. This can be any supported image format. + /// The desired width of the resulting bitmap. + /// The to use should any scaling be required. + /// An instance of the class. + public static Bitmap DecodeToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return new Bitmap(GetFactory().LoadBitmapToWidth(stream, width, interpolationMode)); + } + + /// + /// Loads a Bitmap from a stream and decodes at the desired height. Aspect ratio is maintained. + /// This is more efficient than loading and then resizing. + /// + /// The stream to read the bitmap from. This can be any supported image format. + /// The desired height of the resulting bitmap. + /// The to use should any scaling be required. + /// An instance of the class. + public static Bitmap DecodeToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return new Bitmap(GetFactory().LoadBitmapToHeight(stream, height, interpolationMode)); + } + + /// + /// Creates a Bitmap scaled to a specified size from the current bitmap. + /// + /// The destination size. + /// The to use should any scaling be required. + /// An instance of the class. + public Bitmap CreateScaledBitmap(PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + return new Bitmap(GetFactory().ResizeBitmap(PlatformImpl.Item, destinationSize, interpolationMode)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The filename of the bitmap. + public Bitmap(string fileName) + { + PlatformImpl = RefCountable.Create(GetFactory().LoadBitmap(fileName)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The stream to read the bitmap from. + public Bitmap(Stream stream) + { + PlatformImpl = RefCountable.Create(GetFactory().LoadBitmap(stream)); + } + + /// + /// Initializes a new instance of the class. + /// + /// A platform-specific bitmap implementation. + public Bitmap(IRef impl) + { + PlatformImpl = impl.Clone(); + } + + /// + /// Initializes a new instance of the class. + /// + /// A platform-specific bitmap implementation. Bitmap class takes the ownership. + protected Bitmap(IBitmapImpl impl) + { + PlatformImpl = RefCountable.Create(impl); + } + + /// + public virtual void Dispose() + { + PlatformImpl.Dispose(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The pixel format. + /// The pointer to the source bytes. + /// The size of the bitmap in device pixels. + /// The DPI of the bitmap. + /// The number of bytes per row. + [Obsolete("Use overload taking an AlphaFormat.")] + public Bitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) + { + var ri = GetFactory(); + PlatformImpl = RefCountable.Create(ri + .LoadBitmap(format, ri.DefaultAlphaFormat, data, size, dpi, stride)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The pixel format. + /// The alpha format. + /// The pointer to the source bytes. + /// The size of the bitmap in device pixels. + /// The DPI of the bitmap. + /// The number of bytes per row. + public Bitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride) + { + PlatformImpl = RefCountable.Create(GetFactory().LoadBitmap(format, alphaFormat, data, size, dpi, stride)); + } + + /// + public Vector Dpi => PlatformImpl.Item.Dpi; + + /// + public Size Size => PlatformImpl.Item.PixelSize.ToSizeWithDpi(Dpi); + + /// + public PixelSize PixelSize => PlatformImpl.Item.PixelSize; + + /// + /// Gets the platform-specific bitmap implementation. + /// + public IRef PlatformImpl { get; } + + /// + /// Saves the bitmap to a file. + /// + /// The filename. + public void Save(string fileName) + { + PlatformImpl.Item.Save(fileName); + } + + /// + /// Saves the bitmap to a stream. + /// + /// The stream. + public void Save(Stream stream) + { + PlatformImpl.Item.Save(stream); + } + + /// + void IImage.Draw( + DrawingContext context, + Rect sourceRect, + Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode) + { + context.PlatformImpl.DrawBitmap( + PlatformImpl, + 1, + sourceRect, + destRect, + bitmapInterpolationMode); + } + + private static IPlatformRenderInterface GetFactory() + { + return AvaloniaLocator.Current.GetRequiredService(); + } + } +} diff --git a/src/Avalonia.Base/Media/Imaging/BitmapBlendingMode.cs b/src/Avalonia.Base/Media/Imaging/BitmapBlendingMode.cs new file mode 100644 index 0000000000..eb39020939 --- /dev/null +++ b/src/Avalonia.Base/Media/Imaging/BitmapBlendingMode.cs @@ -0,0 +1,57 @@ +namespace Avalonia.Media.Imaging +{ + /// + /// Controls the way the bitmaps are drawn together. + /// + public enum BitmapBlendingMode + { + /// + /// Source is placed over the destination. + /// + SourceOver, + /// + /// Only the source will be present. + /// + Source, + /// + /// Only the destination will be present. + /// + Destination, + /// + /// Destination is placed over the source. + /// + DestinationOver, + /// + /// The source that overlaps the destination, replaces the destination. + /// + SourceIn, + /// + /// Destination which overlaps the source, replaces the source. + /// + DestinationIn, + /// + /// Source is placed, where it falls outside of the destination. + /// + SourceOut, + /// + /// Destination is placed, where it falls outside of the source. + /// + DestinationOut, + /// + /// Source which overlaps the destination, replaces the destination. + /// + SourceAtop, + /// + /// Destination which overlaps the source replaces the source. + /// + DestinationAtop, + /// + /// The non-overlapping regions of source and destination are combined. + /// + Xor, + /// + /// Display the sum of the source image and destination image. + /// + Plus, + } +} diff --git a/src/Avalonia.Base/Media/Imaging/BitmapInterpolationMode.cs b/src/Avalonia.Base/Media/Imaging/BitmapInterpolationMode.cs new file mode 100644 index 0000000000..7cdb5d8b9f --- /dev/null +++ b/src/Avalonia.Base/Media/Imaging/BitmapInterpolationMode.cs @@ -0,0 +1,28 @@ +namespace Avalonia.Media.Imaging +{ + /// + /// Controls the performance and quality of bitmap scaling. + /// + public enum BitmapInterpolationMode + { + /// + /// Uses the default behavior of the underling render backend. + /// + Default, + + /// + /// The best performance but worst image quality. + /// + LowQuality, + + /// + /// Good performance and decent image quality. + /// + MediumQuality, + + /// + /// Highest quality but worst performance. + /// + HighQuality + } +} diff --git a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs new file mode 100644 index 0000000000..70f9fbf567 --- /dev/null +++ b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs @@ -0,0 +1,96 @@ +using System; +using Avalonia.Media.Imaging; + +namespace Avalonia.Media.Imaging +{ + /// + /// Crops a Bitmap. + /// + public class CroppedBitmap : AvaloniaObject, IImage, IAffectsRender, IDisposable + { + /// + /// Defines the property. + /// + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SourceRectProperty = + AvaloniaProperty.Register(nameof(SourceRect)); + + public event EventHandler? Invalidated; + + static CroppedBitmap() + { + SourceRectProperty.Changed.AddClassHandler((x, e) => x.SourceRectChanged(e)); + SourceProperty.Changed.AddClassHandler((x, e) => x.SourceChanged(e)); + } + + /// + /// Gets or sets the source for the bitmap. + /// + public IImage? Source + { + get => GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + /// + /// Gets or sets the rectangular area that the bitmap is cropped to. + /// + public PixelRect SourceRect + { + get => GetValue(SourceRectProperty); + set => SetValue(SourceRectProperty, value); + } + + public CroppedBitmap() + { + Source = null; + SourceRect = default; + } + + public CroppedBitmap(IImage source, PixelRect sourceRect) + { + Source = source; + SourceRect = sourceRect; + } + + private void SourceChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.NewValue == null) + return; + if (!(e.NewValue is IBitmap)) + throw new ArgumentException("Only IBitmap supported as source"); + Invalidated?.Invoke(this, e); + } + + private void SourceRectChanged(AvaloniaPropertyChangedEventArgs e) => Invalidated?.Invoke(this, e); + + public virtual void Dispose() + { + (Source as IBitmap)?.Dispose(); + } + + public Size Size { + get + { + if (Source is not IBitmap bmp) + return Size.Empty; + if (SourceRect.IsEmpty) + return Source.Size; + return SourceRect.Size.ToSizeWithDpi(bmp.Dpi); + } + } + + public void Draw(DrawingContext context, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + { + if (Source is not IBitmap bmp) + return; + var topLeft = SourceRect.TopLeft.ToPointWithDpi(bmp.Dpi); + Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Imaging/IBitmap.cs b/src/Avalonia.Base/Media/Imaging/IBitmap.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Imaging/IBitmap.cs rename to src/Avalonia.Base/Media/Imaging/IBitmap.cs diff --git a/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs rename to src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs diff --git a/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs b/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs new file mode 100644 index 0000000000..1f39b1344d --- /dev/null +++ b/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs @@ -0,0 +1,97 @@ +using System; +using System.IO; +using Avalonia.Platform; + +namespace Avalonia.Media.Imaging +{ + /// + /// Holds a writeable bitmap image. + /// + public class WriteableBitmap : Bitmap + { + /// + /// Initializes a new instance of the class. + /// + /// The size of the bitmap in device pixels. + /// The DPI of the bitmap. + /// The pixel format (optional). + /// An . + [Obsolete("Use overload taking an AlphaFormat.")] + public WriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null) + : base(CreatePlatformImpl(size, dpi, format, null)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The size of the bitmap in device pixels. + /// The DPI of the bitmap. + /// The pixel format (optional). + /// The alpha format (optional). + /// An . + public WriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat) + : base(CreatePlatformImpl(size, dpi, format, alphaFormat)) + { + } + + private WriteableBitmap(IWriteableBitmapImpl impl) : base(impl) + { + + } + + public ILockedFramebuffer Lock() => ((IWriteableBitmapImpl) PlatformImpl.Item).Lock(); + + public static WriteableBitmap Decode(Stream stream) + { + var ri = GetFactory(); + + return new WriteableBitmap(ri.LoadWriteableBitmap(stream)); + } + + /// + /// Loads a WriteableBitmap from a stream and decodes at the desired width. Aspect ratio is maintained. + /// This is more efficient than loading and then resizing. + /// + /// The stream to read the bitmap from. This can be any supported image format. + /// The desired width of the resulting bitmap. + /// The to use should any scaling be required. + /// An instance of the class. + public new static WriteableBitmap DecodeToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + var ri = GetFactory(); + + return new WriteableBitmap(ri.LoadWriteableBitmapToWidth(stream, width, interpolationMode)); + } + + /// + /// Loads a Bitmap from a stream and decodes at the desired height. Aspect ratio is maintained. + /// This is more efficient than loading and then resizing. + /// + /// The stream to read the bitmap from. This can be any supported image format. + /// The desired height of the resulting bitmap. + /// The to use should any scaling be required. + /// An instance of the class. + public new static WriteableBitmap DecodeToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality) + { + var ri = GetFactory(); + + return new WriteableBitmap(ri.LoadWriteableBitmapToHeight(stream, height, interpolationMode)); + } + + private static IBitmapImpl CreatePlatformImpl(PixelSize size, in Vector dpi, PixelFormat? format, AlphaFormat? alphaFormat) + { + var ri = GetFactory(); + + PixelFormat finalFormat = format ?? ri.DefaultPixelFormat; + AlphaFormat finalAlphaFormat = alphaFormat ?? ri.DefaultAlphaFormat; + + return ri.CreateWriteableBitmap(size, dpi, finalFormat, finalAlphaFormat); + } + + private static IPlatformRenderInterface GetFactory() + { + return AvaloniaLocator.Current.GetRequiredService(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableConicGradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Immutable/ImmutableConicGradientBrush.cs rename to src/Avalonia.Base/Media/Immutable/ImmutableConicGradientBrush.cs diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs rename to src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableGradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs rename to src/Avalonia.Base/Media/Immutable/ImmutableGradientBrush.cs diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientStop.cs b/src/Avalonia.Base/Media/Immutable/ImmutableGradientStop.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Immutable/ImmutableGradientStop.cs rename to src/Avalonia.Base/Media/Immutable/ImmutableGradientStop.cs diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs new file mode 100644 index 0000000000..c36e82eacb --- /dev/null +++ b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs @@ -0,0 +1,63 @@ +using Avalonia.Media.Imaging; + +namespace Avalonia.Media.Immutable +{ + /// + /// Paints an area with an . + /// + internal class ImmutableImageBrush : ImmutableTileBrush, IImageBrush + { + /// + /// Initializes a new instance of the class. + /// + /// The image to draw. + /// The horizontal alignment of a tile in the destination. + /// The vertical alignment of a tile in the destination. + /// The rectangle on the destination in which to paint a tile. + /// The opacity of the brush. + /// The transform of the brush. + /// The rectangle of the source image that will be displayed. + /// + /// How the source rectangle will be stretched to fill the destination rect. + /// + /// The tile mode. + /// The bitmap interpolation mode. + public ImmutableImageBrush( + IBitmap source, + AlignmentX alignmentX = AlignmentX.Center, + AlignmentY alignmentY = AlignmentY.Center, + RelativeRect? destinationRect = null, + double opacity = 1, + ImmutableTransform? transform = null, + RelativeRect? sourceRect = null, + Stretch stretch = Stretch.Uniform, + TileMode tileMode = TileMode.None, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + : base( + alignmentX, + alignmentY, + destinationRect ?? RelativeRect.Fill, + opacity, + transform, + sourceRect ?? RelativeRect.Fill, + stretch, + tileMode, + bitmapInterpolationMode) + { + Source = source; + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush from which this brush's properties should be copied. + public ImmutableImageBrush(IImageBrush source) + : base(source) + { + Source = source.Source; + } + + /// + public IBitmap Source { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableLinearGradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs rename to src/Avalonia.Base/Media/Immutable/ImmutableLinearGradientBrush.cs diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Base/Media/Immutable/ImmutablePen.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs rename to src/Avalonia.Base/Media/Immutable/ImmutablePen.cs diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableRadialGradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs rename to src/Avalonia.Base/Media/Immutable/ImmutableRadialGradientBrush.cs diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableSolidColorBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Immutable/ImmutableSolidColorBrush.cs rename to src/Avalonia.Base/Media/Immutable/ImmutableSolidColorBrush.cs diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableTextDecoration.cs b/src/Avalonia.Base/Media/Immutable/ImmutableTextDecoration.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Immutable/ImmutableTextDecoration.cs rename to src/Avalonia.Base/Media/Immutable/ImmutableTextDecoration.cs diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs new file mode 100644 index 0000000000..2c1844a2c2 --- /dev/null +++ b/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs @@ -0,0 +1,93 @@ +using Avalonia.Media.Imaging; + +namespace Avalonia.Media.Immutable +{ + /// + /// A brush which displays a repeating image. + /// + public abstract class ImmutableTileBrush : ITileBrush + { + /// + /// Initializes a new instance of the class. + /// + /// The horizontal alignment of a tile in the destination. + /// The vertical alignment of a tile in the destination. + /// The rectangle on the destination in which to paint a tile. + /// The opacity of the brush. + /// The transform of the brush. + /// The rectangle of the source image that will be displayed. + /// + /// How the source rectangle will be stretched to fill the destination rect. + /// + /// The tile mode. + /// The bitmap interpolation mode. + protected ImmutableTileBrush( + AlignmentX alignmentX, + AlignmentY alignmentY, + RelativeRect destinationRect, + double opacity, + ImmutableTransform? transform, + RelativeRect sourceRect, + Stretch stretch, + TileMode tileMode, + BitmapInterpolationMode bitmapInterpolationMode) + { + AlignmentX = alignmentX; + AlignmentY = alignmentY; + DestinationRect = destinationRect; + Opacity = opacity; + Transform = transform; + SourceRect = sourceRect; + Stretch = stretch; + TileMode = tileMode; + BitmapInterpolationMode = bitmapInterpolationMode; + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush from which this brush's properties should be copied. + protected ImmutableTileBrush(ITileBrush source) + : this( + source.AlignmentX, + source.AlignmentY, + source.DestinationRect, + source.Opacity, + source.Transform?.ToImmutable(), + source.SourceRect, + source.Stretch, + source.TileMode, + source.BitmapInterpolationMode) + { + } + + /// + public AlignmentX AlignmentX { get; } + + /// + public AlignmentY AlignmentY { get; } + + /// + public RelativeRect DestinationRect { get; } + + /// + public double Opacity { get; } + + /// + /// Gets the transform of the brush. + /// + public ITransform? Transform { get; } + + /// + public RelativeRect SourceRect { get; } + + /// + public Stretch Stretch { get; } + + /// + public TileMode TileMode { get; } + + /// + public BitmapInterpolationMode BitmapInterpolationMode { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableTransform.cs b/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Immutable/ImmutableTransform.cs rename to src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs new file mode 100644 index 0000000000..8ecef63237 --- /dev/null +++ b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs @@ -0,0 +1,64 @@ +using Avalonia.Media.Imaging; +using Avalonia.VisualTree; + +namespace Avalonia.Media.Immutable +{ + /// + /// Paints an area with an . + /// + internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush + { + /// + /// Initializes a new instance of the class. + /// + /// The visual to draw. + /// The horizontal alignment of a tile in the destination. + /// The vertical alignment of a tile in the destination. + /// The rectangle on the destination in which to paint a tile. + /// The opacity of the brush. + /// The transform of the brush. + /// The rectangle of the source image that will be displayed. + /// + /// How the source rectangle will be stretched to fill the destination rect. + /// + /// The tile mode. + /// Controls the quality of interpolation. + public ImmutableVisualBrush( + IVisual visual, + AlignmentX alignmentX = AlignmentX.Center, + AlignmentY alignmentY = AlignmentY.Center, + RelativeRect? destinationRect = null, + double opacity = 1, + ImmutableTransform? transform = null, + RelativeRect? sourceRect = null, + Stretch stretch = Stretch.Uniform, + TileMode tileMode = TileMode.None, + Imaging.BitmapInterpolationMode bitmapInterpolationMode = Imaging.BitmapInterpolationMode.Default) + : base( + alignmentX, + alignmentY, + destinationRect ?? RelativeRect.Fill, + opacity, + transform, + sourceRect ?? RelativeRect.Fill, + stretch, + tileMode, + bitmapInterpolationMode) + { + Visual = visual; + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush from which this brush's properties should be copied. + public ImmutableVisualBrush(IVisualBrush source) + : base(source) + { + Visual = source.Visual; + } + + /// + public IVisual Visual { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Base/Media/ImmutableExperimentalAcrylicMaterial.cs similarity index 100% rename from src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs rename to src/Avalonia.Base/Media/ImmutableExperimentalAcrylicMaterial.cs diff --git a/src/Avalonia.Visuals/Media/KnownColors.cs b/src/Avalonia.Base/Media/KnownColors.cs similarity index 100% rename from src/Avalonia.Visuals/Media/KnownColors.cs rename to src/Avalonia.Base/Media/KnownColors.cs diff --git a/src/Avalonia.Visuals/Media/LineGeometry.cs b/src/Avalonia.Base/Media/LineGeometry.cs similarity index 100% rename from src/Avalonia.Visuals/Media/LineGeometry.cs rename to src/Avalonia.Base/Media/LineGeometry.cs diff --git a/src/Avalonia.Visuals/Media/LineSegment.cs b/src/Avalonia.Base/Media/LineSegment.cs similarity index 100% rename from src/Avalonia.Visuals/Media/LineSegment.cs rename to src/Avalonia.Base/Media/LineSegment.cs diff --git a/src/Avalonia.Visuals/Media/LinearGradientBrush.cs b/src/Avalonia.Base/Media/LinearGradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/LinearGradientBrush.cs rename to src/Avalonia.Base/Media/LinearGradientBrush.cs diff --git a/src/Avalonia.Visuals/Media/MaterialExtensions.cs b/src/Avalonia.Base/Media/MaterialExtensions.cs similarity index 100% rename from src/Avalonia.Visuals/Media/MaterialExtensions.cs rename to src/Avalonia.Base/Media/MaterialExtensions.cs diff --git a/src/Avalonia.Visuals/Media/MatrixTransform.cs b/src/Avalonia.Base/Media/MatrixTransform.cs similarity index 100% rename from src/Avalonia.Visuals/Media/MatrixTransform.cs rename to src/Avalonia.Base/Media/MatrixTransform.cs diff --git a/src/Avalonia.Visuals/Media/MediaExtensions.cs b/src/Avalonia.Base/Media/MediaExtensions.cs similarity index 100% rename from src/Avalonia.Visuals/Media/MediaExtensions.cs rename to src/Avalonia.Base/Media/MediaExtensions.cs diff --git a/src/Avalonia.Visuals/Media/PathFigure.cs b/src/Avalonia.Base/Media/PathFigure.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PathFigure.cs rename to src/Avalonia.Base/Media/PathFigure.cs diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Base/Media/PathGeometry.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PathGeometry.cs rename to src/Avalonia.Base/Media/PathGeometry.cs diff --git a/src/Avalonia.Visuals/Media/PathGeometryCollections.cs b/src/Avalonia.Base/Media/PathGeometryCollections.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PathGeometryCollections.cs rename to src/Avalonia.Base/Media/PathGeometryCollections.cs diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Base/Media/PathMarkupParser.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PathMarkupParser.cs rename to src/Avalonia.Base/Media/PathMarkupParser.cs diff --git a/src/Avalonia.Visuals/Media/PathSegment.cs b/src/Avalonia.Base/Media/PathSegment.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PathSegment.cs rename to src/Avalonia.Base/Media/PathSegment.cs diff --git a/src/Avalonia.Base/Media/Pen.cs b/src/Avalonia.Base/Media/Pen.cs new file mode 100644 index 0000000000..66632a973b --- /dev/null +++ b/src/Avalonia.Base/Media/Pen.cs @@ -0,0 +1,239 @@ +using System; +using Avalonia.Media.Immutable; +using Avalonia.Utilities; + +namespace Avalonia.Media +{ + /// + /// Describes how a stroke is drawn. + /// + public sealed class Pen : AvaloniaObject, IPen + { + /// + /// Defines the property. + /// + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register(nameof(Brush)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ThicknessProperty = + AvaloniaProperty.Register(nameof(Thickness), 1.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DashStyleProperty = + AvaloniaProperty.Register(nameof(DashStyle)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LineCapProperty = + AvaloniaProperty.Register(nameof(LineCap)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LineJoinProperty = + AvaloniaProperty.Register(nameof(LineJoin)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MiterLimitProperty = + AvaloniaProperty.Register(nameof(MiterLimit), 10.0); + + private EventHandler? _invalidated; + private IAffectsRender? _subscribedToBrush; + private IAffectsRender? _subscribedToDashes; + private TargetWeakEventSubscriber? _weakSubscriber; + + /// + /// Initializes a new instance of the class. + /// + public Pen() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The stroke color. + /// The stroke thickness. + /// The dash style. + /// Specifies the type of graphic shape to use on both ends of a line. + /// The line join. + /// The miter limit. + public Pen( + uint color, + double thickness = 1.0, + IDashStyle? dashStyle = null, + PenLineCap lineCap = PenLineCap.Flat, + PenLineJoin lineJoin = PenLineJoin.Miter, + double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush used to draw. + /// The stroke thickness. + /// The dash style. + /// The line cap. + /// The line join. + /// The miter limit. + public Pen( + IBrush? brush, + double thickness = 1.0, + IDashStyle? dashStyle = null, + PenLineCap lineCap = PenLineCap.Flat, + PenLineJoin lineJoin = PenLineJoin.Miter, + double miterLimit = 10.0) + { + Brush = brush; + Thickness = thickness; + LineCap = lineCap; + LineJoin = lineJoin; + MiterLimit = miterLimit; + DashStyle = dashStyle; + } + + /// + /// Gets or sets the brush used to draw the stroke. + /// + public IBrush? Brush + { + get => GetValue(BrushProperty); + set => SetValue(BrushProperty, value); + } + + private static readonly WeakEvent InvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.Invalidated += h, + (s, h) => s.Invalidated -= h); + + /// + /// Gets or sets the stroke thickness. + /// + public double Thickness + { + get => GetValue(ThicknessProperty); + set => SetValue(ThicknessProperty, value); + } + + /// + /// Gets or sets the style of dashed lines drawn with a object. + /// + public IDashStyle? DashStyle + { + get => GetValue(DashStyleProperty); + set => SetValue(DashStyleProperty, value); + } + + /// + /// Gets or sets the type of shape to use on both ends of a line. + /// + public PenLineCap LineCap + { + get => GetValue(LineCapProperty); + set => SetValue(LineCapProperty, value); + } + + /// + /// Gets or sets the join style for the ends of two consecutive lines drawn with this + /// . + /// + public PenLineJoin LineJoin + { + get => GetValue(LineJoinProperty); + set => SetValue(LineJoinProperty, value); + } + + /// + /// Gets or sets the limit of the thickness of the join on a mitered corner. + /// + public double MiterLimit + { + get => GetValue(MiterLimitProperty); + set => SetValue(MiterLimitProperty, value); + } + + /// + /// Raised when the pen changes. + /// + public event EventHandler? Invalidated + { + add + { + _invalidated += value; + UpdateSubscriptions(); + } + remove + { + _invalidated -= value; + UpdateSubscriptions(); + } + } + + /// + /// Creates an immutable clone of the brush. + /// + /// The immutable clone. + public ImmutablePen ToImmutable() + { + return new ImmutablePen( + Brush?.ToImmutable(), + Thickness, + DashStyle?.ToImmutable(), + LineCap, + LineJoin, + MiterLimit); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + _invalidated?.Invoke(this, EventArgs.Empty); + if(change.Property == BrushProperty) + UpdateSubscription(ref _subscribedToBrush, Brush); + if(change.Property == DashStyleProperty) + UpdateSubscription(ref _subscribedToDashes, DashStyle); + base.OnPropertyChanged(change); + } + + + void UpdateSubscription(ref IAffectsRender? field, object? value) + { + if ((_invalidated == null || field != value) && field != null) + { + if (_weakSubscriber != null) + InvalidatedWeakEvent.Unsubscribe(field, _weakSubscriber); + field = null; + } + + if (_invalidated != null && field != value && value is IAffectsRender affectsRender) + { + if (_weakSubscriber == null) + { + _weakSubscriber = new TargetWeakEventSubscriber( + this, static (target, _, ev, _) => + { + if (ev == InvalidatedWeakEvent) + target._invalidated?.Invoke(target, EventArgs.Empty); + }); + } + + InvalidatedWeakEvent.Subscribe(affectsRender, _weakSubscriber); + field = affectsRender; + } + } + + void UpdateSubscriptions() + { + UpdateSubscription(ref _subscribedToBrush, Brush); + UpdateSubscription(ref _subscribedToDashes, DashStyle); + } + } +} diff --git a/src/Avalonia.Visuals/Media/PenLineCap.cs b/src/Avalonia.Base/Media/PenLineCap.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PenLineCap.cs rename to src/Avalonia.Base/Media/PenLineCap.cs diff --git a/src/Avalonia.Visuals/Media/PenLineJoin.cs b/src/Avalonia.Base/Media/PenLineJoin.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PenLineJoin.cs rename to src/Avalonia.Base/Media/PenLineJoin.cs diff --git a/src/Avalonia.Visuals/Media/PolyLineSegment.cs b/src/Avalonia.Base/Media/PolyLineSegment.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PolyLineSegment.cs rename to src/Avalonia.Base/Media/PolyLineSegment.cs diff --git a/src/Avalonia.Visuals/Media/PolylineGeometry.cs b/src/Avalonia.Base/Media/PolylineGeometry.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PolylineGeometry.cs rename to src/Avalonia.Base/Media/PolylineGeometry.cs diff --git a/src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs b/src/Avalonia.Base/Media/PreciseEllipticArcHelper.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs rename to src/Avalonia.Base/Media/PreciseEllipticArcHelper.cs diff --git a/src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs b/src/Avalonia.Base/Media/QuadraticBezierSegment .cs similarity index 100% rename from src/Avalonia.Visuals/Media/QuadraticBezierSegment .cs rename to src/Avalonia.Base/Media/QuadraticBezierSegment .cs diff --git a/src/Avalonia.Visuals/Media/RadialGradientBrush.cs b/src/Avalonia.Base/Media/RadialGradientBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/RadialGradientBrush.cs rename to src/Avalonia.Base/Media/RadialGradientBrush.cs diff --git a/src/Avalonia.Visuals/Media/RectangleGeometry.cs b/src/Avalonia.Base/Media/RectangleGeometry.cs similarity index 100% rename from src/Avalonia.Visuals/Media/RectangleGeometry.cs rename to src/Avalonia.Base/Media/RectangleGeometry.cs diff --git a/src/Avalonia.Base/Media/RenderOptions.cs b/src/Avalonia.Base/Media/RenderOptions.cs new file mode 100644 index 0000000000..5863d0ac58 --- /dev/null +++ b/src/Avalonia.Base/Media/RenderOptions.cs @@ -0,0 +1,36 @@ +using Avalonia.Media.Imaging; + +namespace Avalonia.Media +{ + public class RenderOptions + { + /// + /// Defines the property. + /// + public static readonly StyledProperty BitmapInterpolationModeProperty = + AvaloniaProperty.RegisterAttached( + "BitmapInterpolationMode", + BitmapInterpolationMode.MediumQuality, + inherits: true); + + /// + /// Gets the value of the BitmapInterpolationMode attached property for a control. + /// + /// The control. + /// The control's left coordinate. + public static BitmapInterpolationMode GetBitmapInterpolationMode(AvaloniaObject element) + { + return element.GetValue(BitmapInterpolationModeProperty); + } + + /// + /// Sets the value of the BitmapInterpolationMode attached property for a control. + /// + /// The control. + /// The left value. + public static void SetBitmapInterpolationMode(AvaloniaObject element, BitmapInterpolationMode value) + { + element.SetValue(BitmapInterpolationModeProperty, value); + } + } +} diff --git a/src/Avalonia.Visuals/Media/RotateTransform.cs b/src/Avalonia.Base/Media/RotateTransform.cs similarity index 100% rename from src/Avalonia.Visuals/Media/RotateTransform.cs rename to src/Avalonia.Base/Media/RotateTransform.cs diff --git a/src/Avalonia.Visuals/Media/ScaleTransform.cs b/src/Avalonia.Base/Media/ScaleTransform.cs similarity index 100% rename from src/Avalonia.Visuals/Media/ScaleTransform.cs rename to src/Avalonia.Base/Media/ScaleTransform.cs diff --git a/src/Avalonia.Visuals/Media/SkewTransform.cs b/src/Avalonia.Base/Media/SkewTransform.cs similarity index 100% rename from src/Avalonia.Visuals/Media/SkewTransform.cs rename to src/Avalonia.Base/Media/SkewTransform.cs diff --git a/src/Avalonia.Visuals/Media/SolidColorBrush.cs b/src/Avalonia.Base/Media/SolidColorBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/SolidColorBrush.cs rename to src/Avalonia.Base/Media/SolidColorBrush.cs diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Base/Media/StreamGeometry.cs similarity index 100% rename from src/Avalonia.Visuals/Media/StreamGeometry.cs rename to src/Avalonia.Base/Media/StreamGeometry.cs diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Base/Media/StreamGeometryContext.cs similarity index 100% rename from src/Avalonia.Visuals/Media/StreamGeometryContext.cs rename to src/Avalonia.Base/Media/StreamGeometryContext.cs diff --git a/src/Avalonia.Visuals/Media/Stretch.cs b/src/Avalonia.Base/Media/Stretch.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Stretch.cs rename to src/Avalonia.Base/Media/Stretch.cs diff --git a/src/Avalonia.Visuals/Media/StretchDirection.cs b/src/Avalonia.Base/Media/StretchDirection.cs similarity index 100% rename from src/Avalonia.Visuals/Media/StretchDirection.cs rename to src/Avalonia.Base/Media/StretchDirection.cs diff --git a/src/Avalonia.Visuals/Media/SweepDirection.cs b/src/Avalonia.Base/Media/SweepDirection.cs similarity index 100% rename from src/Avalonia.Visuals/Media/SweepDirection.cs rename to src/Avalonia.Base/Media/SweepDirection.cs diff --git a/src/Avalonia.Visuals/Media/TextAlignment.cs b/src/Avalonia.Base/Media/TextAlignment.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextAlignment.cs rename to src/Avalonia.Base/Media/TextAlignment.cs diff --git a/src/Avalonia.Visuals/Media/TextCollapsingCreateInfo.cs b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextCollapsingCreateInfo.cs rename to src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs diff --git a/src/Avalonia.Visuals/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextDecoration.cs rename to src/Avalonia.Base/Media/TextDecoration.cs diff --git a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs b/src/Avalonia.Base/Media/TextDecorationCollection.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextDecorationCollection.cs rename to src/Avalonia.Base/Media/TextDecorationCollection.cs diff --git a/src/Avalonia.Visuals/Media/TextDecorationLocation.cs b/src/Avalonia.Base/Media/TextDecorationLocation.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextDecorationLocation.cs rename to src/Avalonia.Base/Media/TextDecorationLocation.cs diff --git a/src/Avalonia.Visuals/Media/TextDecorationUnit.cs b/src/Avalonia.Base/Media/TextDecorationUnit.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextDecorationUnit.cs rename to src/Avalonia.Base/Media/TextDecorationUnit.cs diff --git a/src/Avalonia.Visuals/Media/TextDecorations.cs b/src/Avalonia.Base/Media/TextDecorations.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextDecorations.cs rename to src/Avalonia.Base/Media/TextDecorations.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/DrawableTextRun.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs rename to src/Avalonia.Base/Media/TextFormatting/DrawableTextRun.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/FontMetrics.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs rename to src/Avalonia.Base/Media/TextFormatting/FontMetrics.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs rename to src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs b/src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs rename to src/Avalonia.Base/Media/TextFormatting/GenericTextParagraphProperties.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/GenericTextRunProperties.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs rename to src/Avalonia.Base/Media/TextFormatting/GenericTextRunProperties.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ITextSource.cs b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/ITextSource.cs rename to src/Avalonia.Base/Media/TextFormatting/ITextSource.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs b/src/Avalonia.Base/Media/TextFormatting/LogicalDirection.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/LogicalDirection.cs rename to src/Avalonia.Base/Media/TextFormatting/LogicalDirection.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs rename to src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs rename to src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs rename to src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SplitResult.cs b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/SplitResult.cs rename to src/Avalonia.Base/Media/TextFormatting/SplitResult.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextBounds.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs rename to src/Avalonia.Base/Media/TextFormatting/TextBounds.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs rename to src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs rename to src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs rename to src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextEndOfLine.cs rename to src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs b/src/Avalonia.Base/Media/TextFormatting/TextEndOfParagraph.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs rename to src/Avalonia.Base/Media/TextFormatting/TextEndOfParagraph.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs rename to src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs rename to src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs rename to src/Avalonia.Base/Media/TextFormatting/TextLayout.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs rename to src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextLine.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs rename to src/Avalonia.Base/Media/TextFormatting/TextLine.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs rename to src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs rename to src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs rename to src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs rename to src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRange.cs b/src/Avalonia.Base/Media/TextFormatting/TextRange.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextRange.cs rename to src/Avalonia.Base/Media/TextFormatting/TextRange.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs rename to src/Avalonia.Base/Media/TextFormatting/TextRun.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs rename to src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs rename to src/Avalonia.Base/Media/TextFormatting/TextShaper.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs rename to src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs rename to src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs rename to src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiClass.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiClass.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiData.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiPairedBracketType.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiPairedBracketType.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiPairedBracketType.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiPairedBracketType.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GeneralCategory.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GeneralCategory.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/GeneralCategory.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/GeneralCategory.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeBreakClass.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeBreakClass.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeBreakClass.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreak.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakClass.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakClass.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakClass.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakPairTable.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakPairTable.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakPairTable.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakPairTable.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Script.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/Script.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/Script.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeData.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeData.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeData.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrie.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrie.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrie.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrie.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs rename to src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs diff --git a/src/Avalonia.Visuals/Media/TextHitTestResult.cs b/src/Avalonia.Base/Media/TextHitTestResult.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextHitTestResult.cs rename to src/Avalonia.Base/Media/TextHitTestResult.cs diff --git a/src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextLeadingPrefixTrimming.cs rename to src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs diff --git a/src/Avalonia.Visuals/Media/TextNoneTrimming.cs b/src/Avalonia.Base/Media/TextNoneTrimming.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextNoneTrimming.cs rename to src/Avalonia.Base/Media/TextNoneTrimming.cs diff --git a/src/Avalonia.Visuals/Media/TextTrailingTrimming.cs b/src/Avalonia.Base/Media/TextTrailingTrimming.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextTrailingTrimming.cs rename to src/Avalonia.Base/Media/TextTrailingTrimming.cs diff --git a/src/Avalonia.Visuals/Media/TextTrimming.cs b/src/Avalonia.Base/Media/TextTrimming.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextTrimming.cs rename to src/Avalonia.Base/Media/TextTrimming.cs diff --git a/src/Avalonia.Visuals/Media/TextWrapping.cs b/src/Avalonia.Base/Media/TextWrapping.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TextWrapping.cs rename to src/Avalonia.Base/Media/TextWrapping.cs diff --git a/src/Avalonia.Base/Media/TileBrush.cs b/src/Avalonia.Base/Media/TileBrush.cs new file mode 100644 index 0000000000..ab1ee2d604 --- /dev/null +++ b/src/Avalonia.Base/Media/TileBrush.cs @@ -0,0 +1,156 @@ +using Avalonia.Media.Imaging; + +namespace Avalonia.Media +{ + /// + /// Describes how a is tiled. + /// + public enum TileMode + { + /// + /// A single repeat of the content will be displayed. + /// + None, + + /// + /// The content will be repeated horizontally, with alternate tiles mirrored. + /// + FlipX, + + /// + /// The content will be repeated vertically, with alternate tiles mirrored. + /// + FlipY, + + /// + /// The content will be repeated horizontally and vertically, with alternate tiles mirrored. + /// + FlipXY, + + /// + /// The content will be repeated. + /// + Tile + } + + /// + /// Base class for brushes which display repeating images. + /// + public abstract class TileBrush : Brush, ITileBrush + { + /// + /// Defines the property. + /// + public static readonly StyledProperty AlignmentXProperty = + AvaloniaProperty.Register(nameof(AlignmentX), AlignmentX.Center); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AlignmentYProperty = + AvaloniaProperty.Register(nameof(AlignmentY), AlignmentY.Center); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DestinationRectProperty = + AvaloniaProperty.Register(nameof(DestinationRect), RelativeRect.Fill); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SourceRectProperty = + AvaloniaProperty.Register(nameof(SourceRect), RelativeRect.Fill); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StretchProperty = + AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); + + /// + /// Defines the property. + /// + public static readonly StyledProperty TileModeProperty = + AvaloniaProperty.Register(nameof(TileMode)); + + static TileBrush() + { + AffectsRender( + AlignmentXProperty, + AlignmentYProperty, + DestinationRectProperty, + SourceRectProperty, + StretchProperty, + TileModeProperty); + RenderOptions.BitmapInterpolationModeProperty.OverrideDefaultValue(BitmapInterpolationMode.Default); + } + + /// + /// Gets or sets the horizontal alignment of a tile in the destination. + /// + public AlignmentX AlignmentX + { + get { return GetValue(AlignmentXProperty); } + set { SetValue(AlignmentXProperty, value); } + } + + /// + /// Gets or sets the horizontal alignment of a tile in the destination. + /// + public AlignmentY AlignmentY + { + get { return GetValue(AlignmentYProperty); } + set { SetValue(AlignmentYProperty, value); } + } + + /// + /// Gets or sets the rectangle on the destination in which to paint a tile. + /// + public RelativeRect DestinationRect + { + get { return GetValue(DestinationRectProperty); } + set { SetValue(DestinationRectProperty, value); } + } + + /// + /// Gets or sets the rectangle of the source image that will be displayed. + /// + public RelativeRect SourceRect + { + get { return GetValue(SourceRectProperty); } + set { SetValue(SourceRectProperty, value); } + } + + /// + /// Gets or sets a value controlling how the source rectangle will be stretched to fill + /// the destination rect. + /// + public Stretch Stretch + { + get { return (Stretch)GetValue(StretchProperty); } + set { SetValue(StretchProperty, value); } + } + + /// + /// Gets or sets the brush's tile mode. + /// + public TileMode TileMode + { + get { return (TileMode)GetValue(TileModeProperty); } + set { SetValue(TileModeProperty, value); } + } + + /// + /// Gets or sets the bitmap interpolation mode. + /// + /// + /// The bitmap interpolation mode. + /// + public BitmapInterpolationMode BitmapInterpolationMode + { + get { return RenderOptions.GetBitmapInterpolationMode(this); } + set { RenderOptions.SetBitmapInterpolationMode(this, value); } + } + } +} diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Base/Media/Transform.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Transform.cs rename to src/Avalonia.Base/Media/Transform.cs diff --git a/src/Avalonia.Visuals/Media/TransformConverter.cs b/src/Avalonia.Base/Media/TransformConverter.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TransformConverter.cs rename to src/Avalonia.Base/Media/TransformConverter.cs diff --git a/src/Avalonia.Visuals/Media/TransformExtensions.cs b/src/Avalonia.Base/Media/TransformExtensions.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TransformExtensions.cs rename to src/Avalonia.Base/Media/TransformExtensions.cs diff --git a/src/Avalonia.Visuals/Media/TransformGroup.cs b/src/Avalonia.Base/Media/TransformGroup.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TransformGroup.cs rename to src/Avalonia.Base/Media/TransformGroup.cs diff --git a/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs b/src/Avalonia.Base/Media/Transformation/InterpolationUtilities.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs rename to src/Avalonia.Base/Media/Transformation/InterpolationUtilities.cs diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs b/src/Avalonia.Base/Media/Transformation/TransformOperation.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs rename to src/Avalonia.Base/Media/Transformation/TransformOperation.cs diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs b/src/Avalonia.Base/Media/Transformation/TransformOperations.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs rename to src/Avalonia.Base/Media/Transformation/TransformOperations.cs diff --git a/src/Avalonia.Visuals/Media/Transformation/TransformParser.cs b/src/Avalonia.Base/Media/Transformation/TransformParser.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Transformation/TransformParser.cs rename to src/Avalonia.Base/Media/Transformation/TransformParser.cs diff --git a/src/Avalonia.Visuals/Media/TranslateTransform.cs b/src/Avalonia.Base/Media/TranslateTransform.cs similarity index 100% rename from src/Avalonia.Visuals/Media/TranslateTransform.cs rename to src/Avalonia.Base/Media/TranslateTransform.cs diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs similarity index 100% rename from src/Avalonia.Visuals/Media/Typeface.cs rename to src/Avalonia.Base/Media/Typeface.cs diff --git a/src/Avalonia.Visuals/Media/UnicodeRange.cs b/src/Avalonia.Base/Media/UnicodeRange.cs similarity index 100% rename from src/Avalonia.Visuals/Media/UnicodeRange.cs rename to src/Avalonia.Base/Media/UnicodeRange.cs diff --git a/src/Avalonia.Visuals/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs similarity index 100% rename from src/Avalonia.Visuals/Media/VisualBrush.cs rename to src/Avalonia.Base/Media/VisualBrush.cs diff --git a/src/Avalonia.Visuals/Media/PixelPoint.cs b/src/Avalonia.Base/PixelPoint.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PixelPoint.cs rename to src/Avalonia.Base/PixelPoint.cs diff --git a/src/Avalonia.Visuals/Media/PixelRect.cs b/src/Avalonia.Base/PixelRect.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PixelRect.cs rename to src/Avalonia.Base/PixelRect.cs diff --git a/src/Avalonia.Visuals/Media/PixelSize.cs b/src/Avalonia.Base/PixelSize.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PixelSize.cs rename to src/Avalonia.Base/PixelSize.cs diff --git a/src/Avalonia.Visuals/Media/PixelVector.cs b/src/Avalonia.Base/PixelVector.cs similarity index 100% rename from src/Avalonia.Visuals/Media/PixelVector.cs rename to src/Avalonia.Base/PixelVector.cs diff --git a/src/Avalonia.Visuals/Platform/AlphaFormat.cs b/src/Avalonia.Base/Platform/AlphaFormat.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/AlphaFormat.cs rename to src/Avalonia.Base/Platform/AlphaFormat.cs diff --git a/src/Avalonia.Visuals/Platform/IBitmapImpl.cs b/src/Avalonia.Base/Platform/IBitmapImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IBitmapImpl.cs rename to src/Avalonia.Base/Platform/IBitmapImpl.cs diff --git a/src/Avalonia.Input/Platform/ICursorFactory.cs b/src/Avalonia.Base/Platform/ICursorFactory.cs similarity index 100% rename from src/Avalonia.Input/Platform/ICursorFactory.cs rename to src/Avalonia.Base/Platform/ICursorFactory.cs diff --git a/src/Avalonia.Input/Platform/ICursorImpl.cs b/src/Avalonia.Base/Platform/ICursorImpl.cs similarity index 100% rename from src/Avalonia.Input/Platform/ICursorImpl.cs rename to src/Avalonia.Base/Platform/ICursorImpl.cs diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs new file mode 100644 index 0000000000..4e6612e908 --- /dev/null +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -0,0 +1,188 @@ +using System; +using Avalonia.Media; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Avalonia.Media.Imaging; + +namespace Avalonia.Platform +{ + /// + /// Defines the interface through which drawing occurs. + /// + public interface IDrawingContextImpl : IDisposable + { + /// + /// Gets or sets the current transform of the drawing context. + /// + Matrix Transform { get; set; } + + /// + /// Clears the render target to the specified color. + /// + /// The color. + void Clear(Color color); + + /// + /// Draws a bitmap image. + /// + /// The bitmap image. + /// The opacity to draw with. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); + + /// + /// Draws a bitmap image. + /// + /// The bitmap image. + /// The opacity mask to draw with. + /// The destination rect for the opacity mask. + /// The rect in the output to draw to. + void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); + + /// + /// Draws a line. + /// + /// The stroke pen. + /// The first point of the line. + /// The second point of the line. + void DrawLine(IPen pen, Point p1, Point p2); + + /// + /// Draws a geometry. + /// + /// The fill brush. + /// The stroke pen. + /// The geometry. + void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry); + + /// + /// Draws a rectangle with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The rectangle bounds. + /// Box shadow effect parameters + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, + BoxShadows boxShadows = default); + + /// + /// Draws an ellipse with the specified Brush and Pen. + /// + /// The brush used to fill the ellipse, or null for no fill. + /// The pen used to stroke the ellipse, or null for no stroke. + /// The ellipse bounds. + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + void DrawEllipse(IBrush? brush, IPen? pen, Rect rect); + + + /// + /// Draws a glyph run. + /// + /// The foreground. + /// The glyph run. + void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun); + + /// + /// Creates a new that can be used as a render layer + /// for the current render target. + /// + /// The size of the layer in DIPs. + /// An + /// + /// Depending on the rendering backend used, a layer created via this method may be more + /// performant than a standard render target bitmap. In particular the Direct2D backend + /// has to do a format conversion each time a standard render target bitmap is rendered, + /// but a layer created via this method has no such overhead. + /// + IDrawingContextLayerImpl CreateLayer(Size size); + + /// + /// Pushes a clip rectangle. + /// + /// The clip rectangle. + void PushClip(Rect clip); + + /// + /// Pushes a clip rounded rectangle. + /// + /// The clip rounded rectangle + void PushClip(RoundedRect clip); + + /// + /// Pops the latest pushed clip rectangle. + /// + void PopClip(); + + /// + /// Pushes an opacity value. + /// + /// The opacity. + void PushOpacity(double opacity); + + /// + /// Pops the latest pushed opacity value. + /// + void PopOpacity(); + + /// + /// Pushes an opacity mask + /// + void PushOpacityMask(IBrush mask, Rect bounds); + + /// + /// Pops the latest pushed opacity mask. + /// + void PopOpacityMask(); + + /// + /// Pushes a clip geometry. + /// + /// The clip geometry. + void PushGeometryClip(IGeometryImpl clip); + + /// + /// Pops the latest pushed geometry clip. + /// + void PopGeometryClip(); + + /// + /// Pushes a bitmap blending value. + /// + /// The bitmap blending mode. + void PushBitmapBlendMode(BitmapBlendingMode blendingMode); + + /// + /// Pops the latest pushed bitmap blending value. + /// + void PopBitmapBlendMode(); + + /// + /// Adds a custom draw operation + /// + /// Custom draw operation + void Custom(ICustomDrawOperation custom); + } + + public interface IDrawingContextLayerImpl : IRenderTargetBitmapImpl + { + /// + /// Does optimized blit with Src blend mode + /// + /// + void Blit(IDrawingContextImpl context); + + /// + /// Returns true if layer supports optimized blit + /// + bool CanBlit { get; } + } +} diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs b/src/Avalonia.Base/Platform/IDrawingContextWithAcrylicLikeSupport.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs rename to src/Avalonia.Base/Platform/IDrawingContextWithAcrylicLikeSupport.cs diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IFontManagerImpl.cs rename to src/Avalonia.Base/Platform/IFontManagerImpl.cs diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Base/Platform/IGeometryContext.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IGeometryContext.cs rename to src/Avalonia.Base/Platform/IGeometryContext.cs diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Base/Platform/IGeometryImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IGeometryImpl.cs rename to src/Avalonia.Base/Platform/IGeometryImpl.cs diff --git a/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs rename to src/Avalonia.Base/Platform/IGlyphRunImpl.cs diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Base/Platform/IGlyphTypefaceImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs rename to src/Avalonia.Base/Platform/IGlyphTypefaceImpl.cs diff --git a/src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs b/src/Avalonia.Base/Platform/ILockedFramebuffer.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs rename to src/Avalonia.Base/Platform/ILockedFramebuffer.cs diff --git a/src/Avalonia.Visuals/Platform/IModuleEnvironmentChecker.cs b/src/Avalonia.Base/Platform/IModuleEnvironmentChecker.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IModuleEnvironmentChecker.cs rename to src/Avalonia.Base/Platform/IModuleEnvironmentChecker.cs diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs new file mode 100644 index 0000000000..c46efd46c3 --- /dev/null +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Avalonia.Media; +using Avalonia.Media.Imaging; + +namespace Avalonia.Platform +{ + /// + /// Defines the main platform-specific interface for the rendering subsystem. + /// + public interface IPlatformRenderInterface + { + /// + /// Creates an ellipse geometry implementation. + /// + /// The bounds of the ellipse. + /// An ellipse geometry.. + IGeometryImpl CreateEllipseGeometry(Rect rect); + + /// + /// Creates a line geometry implementation. + /// + /// The start of the line. + /// The end of the line. + /// A line geometry. + IGeometryImpl CreateLineGeometry(Point p1, Point p2); + + /// + /// Creates a rectangle geometry implementation. + /// + /// The bounds of the rectangle. + /// A rectangle. + IGeometryImpl CreateRectangleGeometry(Rect rect); + + /// + /// Creates a stream geometry implementation. + /// + /// An . + IStreamGeometryImpl CreateStreamGeometry(); + + /// + /// Creates a geometry group implementation. + /// + /// The fill rule. + /// The geometries to group. + /// A combined geometry. + IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); + + /// + /// Creates a geometry group implementation. + /// + /// The combine mode + /// The first geometry. + /// The second geometry. + /// A combined geometry. + IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); + + /// + /// Creates a renderer. + /// + /// + /// The list of native platform surfaces that can be used for output. + /// + /// An . + IRenderTarget CreateRenderTarget(IEnumerable surfaces); + + /// + /// Creates a render target bitmap implementation. + /// + /// The size of the bitmap in device pixels. + /// The DPI of the bitmap. + /// An . + IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi); + + /// + /// Creates a writeable bitmap implementation. + /// + /// The size of the bitmap in device pixels. + /// The DPI of the bitmap. + /// Pixel format. + /// Alpha format . + /// An . + IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat); + + /// + /// Loads a bitmap implementation from a file.. + /// + /// The filename of the bitmap. + /// An . + IBitmapImpl LoadBitmap(string fileName); + + /// + /// Loads a bitmap implementation from a file.. + /// + /// The stream to read the bitmap from. + /// An . + IBitmapImpl LoadBitmap(Stream stream); + + /// + /// Loads a WriteableBitmap implementation from a stream to a specified width maintaining aspect ratio. + /// + /// The stream to read the bitmap from. + /// The desired width of the resulting bitmap. + /// The to use should resizing be required. + /// An . + IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + + /// + /// Loads a WriteableBitmap implementation from a stream to a specified height maintaining aspect ratio. + /// + /// The stream to read the bitmap from. + /// The desired height of the resulting bitmap. + /// The to use should resizing be required. + /// An . + IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + + /// + /// Loads a WriteableBitmap implementation from a file. + /// + /// The filename of the bitmap. + /// An . + IWriteableBitmapImpl LoadWriteableBitmap(string fileName); + + /// + /// Loads a WriteableBitmap implementation from a file. + /// + /// The stream to read the bitmap from. + /// An . + IWriteableBitmapImpl LoadWriteableBitmap(Stream stream); + + /// + /// Loads a bitmap implementation from a stream to a specified width maintaining aspect ratio. + /// + /// The stream to read the bitmap from. + /// The desired width of the resulting bitmap. + /// The to use should resizing be required. + /// An . + IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + + /// + /// Loads a bitmap implementation from a stream to a specified height maintaining aspect ratio. + /// + /// The stream to read the bitmap from. + /// The desired height of the resulting bitmap. + /// The to use should resizing be required. + /// An . + IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + + IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality); + + /// + /// Loads a bitmap implementation from a pixels in memory. + /// + /// The pixel format. + /// The alpha format. + /// The pointer to source bytes. + /// The size of the bitmap in device pixels. + /// The DPI of the bitmap. + /// The number of bytes per row. + /// An . + IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride); + + /// + /// Creates a platform implementation of a glyph run. + /// + /// The glyph run. + /// + IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun); + + /// + /// Gets a value indicating whether the platform directly supports rectangles with rounded corners. + /// + /// + /// Some platform renderers can't directly handle rounded corners on rectangles. + /// In this case, code that requires rounded corners must generate and retain a geometry instead. + /// + bool SupportsIndividualRoundRects { get; } + + /// + /// Default used on this platform. + /// + public AlphaFormat DefaultAlphaFormat { get; } + + /// + /// Default used on this platform. + /// + public PixelFormat DefaultPixelFormat { get; } + } +} diff --git a/src/Avalonia.Visuals/Platform/IPlatformSettings.cs b/src/Avalonia.Base/Platform/IPlatformSettings.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IPlatformSettings.cs rename to src/Avalonia.Base/Platform/IPlatformSettings.cs diff --git a/src/Avalonia.Visuals/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IRenderTarget.cs rename to src/Avalonia.Base/Platform/IRenderTarget.cs diff --git a/src/Avalonia.Visuals/Platform/IRenderTargetBitmapImpl.cs b/src/Avalonia.Base/Platform/IRenderTargetBitmapImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IRenderTargetBitmapImpl.cs rename to src/Avalonia.Base/Platform/IRenderTargetBitmapImpl.cs diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs b/src/Avalonia.Base/Platform/IStreamGeometryContextImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs rename to src/Avalonia.Base/Platform/IStreamGeometryContextImpl.cs diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryImpl.cs b/src/Avalonia.Base/Platform/IStreamGeometryImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IStreamGeometryImpl.cs rename to src/Avalonia.Base/Platform/IStreamGeometryImpl.cs diff --git a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs b/src/Avalonia.Base/Platform/ITextShaperImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/ITextShaperImpl.cs rename to src/Avalonia.Base/Platform/ITextShaperImpl.cs diff --git a/src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs b/src/Avalonia.Base/Platform/ITransformedGeometryImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs rename to src/Avalonia.Base/Platform/ITransformedGeometryImpl.cs diff --git a/src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs b/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs rename to src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs diff --git a/src/Avalonia.Visuals/Platform/LockedFramebuffer.cs b/src/Avalonia.Base/Platform/LockedFramebuffer.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/LockedFramebuffer.cs rename to src/Avalonia.Base/Platform/LockedFramebuffer.cs diff --git a/src/Avalonia.Visuals/Platform/PathGeometryContext.cs b/src/Avalonia.Base/Platform/PathGeometryContext.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/PathGeometryContext.cs rename to src/Avalonia.Base/Platform/PathGeometryContext.cs diff --git a/src/Avalonia.Visuals/Platform/PixelFormat.cs b/src/Avalonia.Base/Platform/PixelFormat.cs similarity index 100% rename from src/Avalonia.Visuals/Platform/PixelFormat.cs rename to src/Avalonia.Base/Platform/PixelFormat.cs diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Base/Point.cs similarity index 100% rename from src/Avalonia.Visuals/Point.cs rename to src/Avalonia.Base/Point.cs diff --git a/src/Avalonia.Visuals/Points.cs b/src/Avalonia.Base/Points.cs similarity index 100% rename from src/Avalonia.Visuals/Points.cs rename to src/Avalonia.Base/Points.cs diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index 9ffb5872f0..a0560924e7 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -1,15 +1,33 @@ -// Licensed under the MIT license. See licence.md file in the project root for full license information. - using System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Metadata; +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.TextInput")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.LogicalTree")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Transformation")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Styling")] + [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] +[assembly: InternalsVisibleTo("Avalonia.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -[assembly: InternalsVisibleTo("Avalonia.Visuals, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.PlatformSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - +[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.Web.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs index cffbaed6b0..9a25e98a23 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs @@ -18,19 +18,19 @@ namespace Avalonia.PropertyStore /// The property type. internal class BindingEntry : IBindingEntry, IPriorityValueEntry, IObserver> { - private readonly IAvaloniaObject _owner; - private IValueSink _sink; + private readonly AvaloniaObject _owner; + private ValueOwner _sink; private IDisposable? _subscription; private bool _isSubscribed; private bool _batchUpdate; private Optional _value; public BindingEntry( - IAvaloniaObject owner, + AvaloniaObject owner, StyledPropertyBase property, IObservable> source, BindingPriority priority, - IValueSink sink) + ValueOwner sink) { _owner = owner; Property = property; @@ -50,7 +50,7 @@ namespace Avalonia.PropertyStore { _batchUpdate = false; - if (_sink is ValueStore) + if (_sink.IsValueStore) Start(); } @@ -113,16 +113,15 @@ namespace Avalonia.PropertyStore } } - public void Reparent(IValueSink sink) => _sink = sink; + public void Reparent(PriorityValue parent) => _sink = new(parent); public void RaiseValueChanged( - IValueSink sink, - IAvaloniaObject owner, + AvaloniaObject owner, AvaloniaProperty property, Optional oldValue, Optional newValue) { - sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( owner, (AvaloniaProperty)property, oldValue.Cast(), diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index c7fbf56abc..4116f4abd9 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -18,14 +18,14 @@ namespace Avalonia.PropertyStore /// The property type. internal class ConstantValueEntry : IPriorityValueEntry, IConstantValueEntry { - private IValueSink _sink; + private ValueOwner _sink; private Optional _value; public ConstantValueEntry( StyledPropertyBase property, T value, BindingPriority priority, - IValueSink sink) + ValueOwner sink) { Property = property; _value = value; @@ -37,7 +37,7 @@ namespace Avalonia.PropertyStore StyledPropertyBase property, Optional value, BindingPriority priority, - IValueSink sink) + ValueOwner sink) { Property = property; _value = value; @@ -62,17 +62,16 @@ namespace Avalonia.PropertyStore _sink.Completed(Property, this, oldValue); } - public void Reparent(IValueSink sink) => _sink = sink; + public void Reparent(PriorityValue sink) => _sink = new(sink); public void Start() { } public void RaiseValueChanged( - IValueSink sink, - IAvaloniaObject owner, + AvaloniaObject owner, AvaloniaProperty property, Optional oldValue, Optional newValue) { - sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( owner, (AvaloniaProperty)property, oldValue.Cast(), diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs index 26665ab683..45bbd0cda5 100644 --- a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs @@ -5,7 +5,6 @@ /// internal interface IPriorityValueEntry : IValue { - void Reparent(IValueSink sink); } /// @@ -14,5 +13,6 @@ /// The property type. internal interface IPriorityValueEntry : IPriorityValueEntry, IValue { + void Reparent(PriorityValue parent); } } diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs index a4ec06e64e..b493df92e6 100644 --- a/src/Avalonia.Base/PropertyStore/IValue.cs +++ b/src/Avalonia.Base/PropertyStore/IValue.cs @@ -11,8 +11,7 @@ namespace Avalonia.PropertyStore Optional GetValue(); void Start(); void RaiseValueChanged( - IValueSink sink, - IAvaloniaObject owner, + AvaloniaObject owner, AvaloniaProperty property, Optional oldValue, Optional newValue); diff --git a/src/Avalonia.Base/PropertyStore/IValueSink.cs b/src/Avalonia.Base/PropertyStore/IValueSink.cs deleted file mode 100644 index 738fdef267..0000000000 --- a/src/Avalonia.Base/PropertyStore/IValueSink.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Avalonia.Data; - -namespace Avalonia.PropertyStore -{ - /// - /// Represents an entity that can receive change notifications in a . - /// - internal interface IValueSink - { - void ValueChanged(AvaloniaPropertyChangedEventArgs change); - - void Completed( - StyledPropertyBase property, - IPriorityValueEntry entry, - Optional oldValue); - } -} diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index cb5f3556d1..13ca69681f 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -25,13 +25,12 @@ namespace Avalonia.PropertyStore public void Start() { } public void RaiseValueChanged( - IValueSink sink, - IAvaloniaObject owner, + AvaloniaObject owner, AvaloniaProperty property, Optional oldValue, Optional newValue) { - sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( owner, (AvaloniaProperty)property, oldValue.Cast(), diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index f670160e75..112cf6619f 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -1,10 +1,17 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using Avalonia.Data; namespace Avalonia.PropertyStore { + /// + /// Represents an untyped interface to . + /// + interface IPriorityValue : IValue + { + void UpdateEffectiveValue(); + } + /// /// Stores a set of prioritized values and bindings in a . /// @@ -16,10 +23,10 @@ namespace Avalonia.PropertyStore /// entries (sorted first by priority and then in the order /// they were added) plus a local value. /// - internal class PriorityValue : IValue, IValueSink, IBatchUpdate + internal class PriorityValue : IPriorityValue, IValue, IBatchUpdate { - private readonly IAvaloniaObject _owner; - private readonly IValueSink _sink; + private readonly AvaloniaObject _owner; + private readonly ValueStore _store; private readonly List> _entries = new List>(); private readonly Func? _coerceValue; private Optional _localValue; @@ -28,13 +35,13 @@ namespace Avalonia.PropertyStore private bool _batchUpdate; public PriorityValue( - IAvaloniaObject owner, + AvaloniaObject owner, StyledPropertyBase property, - IValueSink sink) + ValueStore store) { _owner = owner; Property = property; - _sink = sink; + _store = store; if (property.HasCoercion) { @@ -44,11 +51,11 @@ namespace Avalonia.PropertyStore } public PriorityValue( - IAvaloniaObject owner, + AvaloniaObject owner, StyledPropertyBase property, - IValueSink sink, + ValueStore store, IPriorityValueEntry existing) - : this(owner, property, sink) + : this(owner, property, store) { existing.Reparent(this); _entries.Add(existing); @@ -75,9 +82,9 @@ namespace Avalonia.PropertyStore } public PriorityValue( - IAvaloniaObject owner, + AvaloniaObject owner, StyledPropertyBase property, - IValueSink sink, + ValueStore sink, LocalValueEntry existing) : this(owner, property, sink) { @@ -148,7 +155,7 @@ namespace Avalonia.PropertyStore else { var insert = FindInsertPoint(priority); - var entry = new ConstantValueEntry(Property, value, priority, this); + var entry = new ConstantValueEntry(Property, value, priority, new ValueOwner(this)); _entries.Insert(insert, entry); result = entry; } @@ -165,7 +172,7 @@ namespace Avalonia.PropertyStore public BindingEntry AddBinding(IObservable> source, BindingPriority priority) { - var binding = new BindingEntry(_owner, Property, source, priority, this); + var binding = new BindingEntry(_owner, Property, source, priority, new(this)); var insert = FindInsertPoint(binding.Priority); _entries.Insert(insert, binding); @@ -186,13 +193,12 @@ namespace Avalonia.PropertyStore public void Start() => UpdateEffectiveValue(null); public void RaiseValueChanged( - IValueSink sink, - IAvaloniaObject owner, + AvaloniaObject owner, AvaloniaProperty property, Optional oldValue, Optional newValue) { - sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( owner, (AvaloniaProperty)property, oldValue.Cast(), @@ -200,7 +206,7 @@ namespace Avalonia.PropertyStore Priority)); } - void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) + public void ValueChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Priority == BindingPriority.LocalValue) { @@ -213,22 +219,15 @@ namespace Avalonia.PropertyStore } } - void IValueSink.Completed( - StyledPropertyBase property, - IPriorityValueEntry entry, - Optional oldValue) + public void Completed(IPriorityValueEntry entry, Optional oldValue) { _entries.Remove((IPriorityValueEntry)entry); - - if (oldValue is Optional o) - { - UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( - _owner, - Property, - o, - default, - entry.Priority)); - } + UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( + _owner, + Property, + oldValue, + default, + entry.Priority)); } private int FindInsertPoint(BindingPriority priority) @@ -308,7 +307,7 @@ namespace Avalonia.PropertyStore var old = _value; _value = value; - _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _store.ValueChanged(new AvaloniaPropertyChangedEventArgs( _owner, Property, old, @@ -319,7 +318,7 @@ namespace Avalonia.PropertyStore { change.MarkNonEffectiveValue(); change.SetOldValue(default); - _sink.ValueChanged(change); + _store.ValueChanged(change); } } } diff --git a/src/Avalonia.Base/PropertyStore/ValueOwner.cs b/src/Avalonia.Base/PropertyStore/ValueOwner.cs new file mode 100644 index 0000000000..c68435f7a5 --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/ValueOwner.cs @@ -0,0 +1,45 @@ +using Avalonia.Data; + +namespace Avalonia.PropertyStore +{ + /// + /// Represents a union type of and , + /// which are the valid owners of a value store . + /// + /// The value type. + internal readonly struct ValueOwner + { + private readonly ValueStore? _store; + private readonly PriorityValue? _priorityValue; + + public ValueOwner(ValueStore o) + { + _store = o; + _priorityValue = null; + } + + public ValueOwner(PriorityValue v) + { + _store = null; + _priorityValue = v; + } + + public bool IsValueStore => _store is not null; + + public void Completed(StyledPropertyBase property, IPriorityValueEntry entry, Optional oldValue) + { + if (_store is not null) + _store?.Completed(property, entry, oldValue); + else + _priorityValue!.Completed(entry, oldValue); + } + + public void ValueChanged(AvaloniaPropertyChangedEventArgs e) + { + if (_store is not null) + _store?.ValueChanged(e); + else + _priorityValue!.ValueChanged(e); + } + } +} diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs index 238bb16e3e..9f038214c4 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs @@ -51,7 +51,7 @@ namespace Avalonia.Reactive { if (e is AvaloniaPropertyChangedEventArgs typedArgs) { - var newValue = e.Sender.GetValue(typedArgs.Property); + var newValue = e.Sender.GetValue(typedArgs.Property); if (!_value.HasValue || !EqualityComparer.Default.Equals(newValue, _value.Value)) { diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs index fbc126bb05..cafce58c9c 100644 --- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs +++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs @@ -49,23 +49,31 @@ namespace Avalonia.Reactive { if (e.Property == _property) { - T newValue; - - if (e is AvaloniaPropertyChangedEventArgs typed) + if (e.Sender is AvaloniaObject ao) { - newValue = typed.Sender.GetValue(typed.Property); + T newValue; + + if (e is AvaloniaPropertyChangedEventArgs typed) + { + newValue = AvaloniaObjectExtensions.GetValue(ao, typed.Property); + } + else + { + newValue = (T)e.Sender.GetValue(e.Property)!; + } + + if (!_value.HasValue || + !EqualityComparer.Default.Equals(newValue, _value.Value)) + { + _value = newValue; + PublishNext(_value.Value!); + } } else { - newValue = (T)e.Sender.GetValue(e.Property)!; + throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); } - if (!_value.HasValue || - !EqualityComparer.Default.Equals(newValue, _value.Value)) - { - _value = newValue; - PublishNext(_value.Value!); - } } } } diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Base/Rect.cs similarity index 100% rename from src/Avalonia.Visuals/Rect.cs rename to src/Avalonia.Base/Rect.cs diff --git a/src/Avalonia.Base/RelativePoint.cs b/src/Avalonia.Base/RelativePoint.cs new file mode 100644 index 0000000000..e1fd0093b6 --- /dev/null +++ b/src/Avalonia.Base/RelativePoint.cs @@ -0,0 +1,205 @@ +using System; +using System.Globalization; +#if !BUILDTASK +using Avalonia.Animation.Animators; +#endif +using Avalonia.Utilities; + +namespace Avalonia +{ + /// + /// Defines the reference point units of an or + /// . + /// +#if !BUILDTASK + public +#endif + enum RelativeUnit + { + /// + /// The point is expressed as a fraction of the containing element's size. + /// + Relative, + + /// + /// The point is absolute (i.e. in pixels). + /// + Absolute, + } + + /// + /// Defines a point that may be defined relative to a containing element. + /// +#if !BUILDTASK + public +#endif + readonly struct RelativePoint : IEquatable + { + /// + /// A point at the top left of the containing element. + /// + public static readonly RelativePoint TopLeft = new RelativePoint(0, 0, RelativeUnit.Relative); + + /// + /// A point at the center of the containing element. + /// + public static readonly RelativePoint Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); + + /// + /// A point at the bottom right of the containing element. + /// + public static readonly RelativePoint BottomRight = new RelativePoint(1, 1, RelativeUnit.Relative); + + private readonly Point _point; + + private readonly RelativeUnit _unit; + + static RelativePoint() + { +#if !BUILDTASK + Animation.Animation.RegisterAnimator(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType)); +#endif + } + + /// + /// Initializes a new instance of the struct. + /// + /// The X point. + /// The Y point + /// The unit. + public RelativePoint(double x, double y, RelativeUnit unit) + : this(new Point(x, y), unit) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The point. + /// The unit. + public RelativePoint(Point point, RelativeUnit unit) + { + _point = point; + _unit = unit; + } + + /// + /// Gets the point. + /// + public Point Point => _point; + + /// + /// Gets the unit. + /// + public RelativeUnit Unit => _unit; + + /// + /// Checks for equality between two s. + /// + /// The first point. + /// The second point. + /// True if the points are equal; otherwise false. + public static bool operator ==(RelativePoint left, RelativePoint right) + { + return left.Equals(right); + } + + /// + /// Checks for inequality between two s. + /// + /// The first point. + /// The second point. + /// True if the points are unequal; otherwise false. + public static bool operator !=(RelativePoint left, RelativePoint right) + { + return !left.Equals(right); + } + + /// + /// Checks if the equals another object. + /// + /// The other object. + /// True if the objects are equal, otherwise false. + public override bool Equals(object? obj) => obj is RelativePoint other && Equals(other); + + /// + /// Checks if the equals another point. + /// + /// The other point. + /// True if the objects are equal, otherwise false. + public bool Equals(RelativePoint p) + { + return Unit == p.Unit && Point == p.Point; + } + + /// + /// Gets a hashcode for a . + /// + /// A hash code. + public override int GetHashCode() + { + unchecked + { + return (_point.GetHashCode() * 397) ^ (int)_unit; + } + } + + /// + /// Converts a into pixels. + /// + /// The size of the visual. + /// The origin point in pixels. + public Point ToPixels(Size size) + { + return _unit == RelativeUnit.Absolute ? + _point : + new Point(_point.X * size.Width, _point.Y * size.Height); + } + + /// + /// Parses a string. + /// + /// The string. + /// The parsed . + public static RelativePoint Parse(string s) + { + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid RelativePoint.")) + { + var x = tokenizer.ReadString(); + var y = tokenizer.ReadString(); + + var unit = RelativeUnit.Absolute; + var scale = 1.0; + + if (x.EndsWith("%")) + { + if (!y.EndsWith("%")) + { + throw new FormatException("If one coordinate is relative, both must be."); + } + + x = x.TrimEnd('%'); + y = y.TrimEnd('%'); + unit = RelativeUnit.Relative; + scale = 0.01; + } + + return new RelativePoint( + double.Parse(x, CultureInfo.InvariantCulture) * scale, + double.Parse(y, CultureInfo.InvariantCulture) * scale, + unit); + } + } + + /// + /// Returns a String representing this RelativePoint instance. + /// + /// The string representation. + public override string ToString() + { + return _unit == RelativeUnit.Absolute ? + _point.ToString() : + string.Format(CultureInfo.InvariantCulture, "{0}%, {1}%", _point.X * 100, _point.Y * 100); + } + } +} diff --git a/src/Avalonia.Visuals/RelativeRect.cs b/src/Avalonia.Base/RelativeRect.cs similarity index 100% rename from src/Avalonia.Visuals/RelativeRect.cs rename to src/Avalonia.Base/RelativeRect.cs diff --git a/src/Avalonia.Visuals/RenderTargetCorruptedException.cs b/src/Avalonia.Base/RenderTargetCorruptedException.cs similarity index 100% rename from src/Avalonia.Visuals/RenderTargetCorruptedException.cs rename to src/Avalonia.Base/RenderTargetCorruptedException.cs diff --git a/src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/DefaultRenderTimer.cs rename to src/Avalonia.Base/Rendering/DefaultRenderTimer.cs diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/DeferredRenderer.cs rename to src/Avalonia.Base/Rendering/DeferredRenderer.cs diff --git a/src/Avalonia.Visuals/Rendering/DirtyRects.cs b/src/Avalonia.Base/Rendering/DirtyRects.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/DirtyRects.cs rename to src/Avalonia.Base/Rendering/DirtyRects.cs diff --git a/src/Avalonia.Visuals/Rendering/DirtyVisuals.cs b/src/Avalonia.Base/Rendering/DirtyVisuals.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/DirtyVisuals.cs rename to src/Avalonia.Base/Rendering/DirtyVisuals.cs diff --git a/src/Avalonia.Visuals/Rendering/DisplayDirtyRect.cs b/src/Avalonia.Base/Rendering/DisplayDirtyRect.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/DisplayDirtyRect.cs rename to src/Avalonia.Base/Rendering/DisplayDirtyRect.cs diff --git a/src/Avalonia.Visuals/Rendering/DisplayDirtyRects.cs b/src/Avalonia.Base/Rendering/DisplayDirtyRects.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/DisplayDirtyRects.cs rename to src/Avalonia.Base/Rendering/DisplayDirtyRects.cs diff --git a/src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs b/src/Avalonia.Base/Rendering/ICustomSimpleHitTest.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/ICustomSimpleHitTest.cs rename to src/Avalonia.Base/Rendering/ICustomSimpleHitTest.cs diff --git a/src/Avalonia.Visuals/Rendering/IDeferredRendererLock.cs b/src/Avalonia.Base/Rendering/IDeferredRendererLock.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/IDeferredRendererLock.cs rename to src/Avalonia.Base/Rendering/IDeferredRendererLock.cs diff --git a/src/Avalonia.Visuals/Rendering/IRenderLoop.cs b/src/Avalonia.Base/Rendering/IRenderLoop.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/IRenderLoop.cs rename to src/Avalonia.Base/Rendering/IRenderLoop.cs diff --git a/src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs b/src/Avalonia.Base/Rendering/IRenderLoopTask.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/IRenderLoopTask.cs rename to src/Avalonia.Base/Rendering/IRenderLoopTask.cs diff --git a/src/Avalonia.Visuals/Rendering/IRenderRoot.cs b/src/Avalonia.Base/Rendering/IRenderRoot.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/IRenderRoot.cs rename to src/Avalonia.Base/Rendering/IRenderRoot.cs diff --git a/src/Avalonia.Visuals/Rendering/IRenderTimer.cs b/src/Avalonia.Base/Rendering/IRenderTimer.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/IRenderTimer.cs rename to src/Avalonia.Base/Rendering/IRenderTimer.cs diff --git a/src/Avalonia.Visuals/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/IRenderer.cs rename to src/Avalonia.Base/Rendering/IRenderer.cs diff --git a/src/Avalonia.Visuals/Rendering/IRendererFactory.cs b/src/Avalonia.Base/Rendering/IRendererFactory.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/IRendererFactory.cs rename to src/Avalonia.Base/Rendering/IRendererFactory.cs diff --git a/src/Avalonia.Visuals/Rendering/IVisualBrushInitialize.cs b/src/Avalonia.Base/Rendering/IVisualBrushInitialize.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/IVisualBrushInitialize.cs rename to src/Avalonia.Base/Rendering/IVisualBrushInitialize.cs diff --git a/src/Avalonia.Visuals/Rendering/IVisualBrushRenderer.cs b/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/IVisualBrushRenderer.cs rename to src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs new file mode 100644 index 0000000000..2c0298affa --- /dev/null +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Logging; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering +{ + /// + /// A renderer which renders the state of the visual tree without an intermediate scene graph + /// representation. + /// + /// + /// The immediate renderer supports only clip-bound-based hit testing; a control's geometry is + /// not taken into account. + /// + public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer + { + private readonly IVisual _root; + private readonly IRenderRoot? _renderRoot; + private bool _updateTransformedBounds = true; + private IRenderTarget? _renderTarget; + + /// + /// Initializes a new instance of the class. + /// + /// The control to render. + public ImmediateRenderer(IVisual root) + { + _root = root ?? throw new ArgumentNullException(nameof(root)); + _renderRoot = root as IRenderRoot; + } + + private ImmediateRenderer(IVisual root, bool updateTransformedBounds) + { + _root = root ?? throw new ArgumentNullException(nameof(root)); + _renderRoot = root as IRenderRoot; + _updateTransformedBounds = updateTransformedBounds; + } + + /// + public bool DrawFps { get; set; } + + /// + public bool DrawDirtyRects { get; set; } + + /// + public event EventHandler? SceneInvalidated; + + /// + public void Paint(Rect rect) + { + if (_renderTarget == null) + { + _renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + } + + try + { + using (var context = new DrawingContext(_renderTarget.CreateDrawingContext(this))) + { + context.PlatformImpl.Clear(Colors.Transparent); + + using (context.PushTransformContainer()) + { + Render(context, _root, _root.Bounds); + } + + if (DrawDirtyRects) + { + var color = (uint)new Random().Next(0xffffff) | 0x44000000; + context.FillRectangle( + new SolidColorBrush(color), + rect); + } + + if (DrawFps) + { + RenderFps(context, _root.Bounds, null); + } + } + } + catch (RenderTargetCorruptedException ex) + { + Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex); + _renderTarget.Dispose(); + _renderTarget = null; + } + + SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect)); + } + + /// + public void Resized(Size size) + { + } + + /// + /// Renders a visual to a render target. + /// + /// The visual. + /// The render target. + public static void Render(IVisual visual, IRenderTarget target) + { + using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) + using (var context = new DrawingContext(target.CreateDrawingContext(renderer))) + { + renderer.Render(context, visual, visual.Bounds); + } + } + + /// + /// Renders a visual to a drawing context. + /// + /// The visual. + /// The drawing context. + public static void Render(IVisual visual, DrawingContext context) + { + using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) + { + renderer.Render(context, visual, visual.Bounds); + } + } + + /// + public void AddDirty(IVisual visual) + { + if (visual.Bounds != Rect.Empty) + { + var m = visual.TransformToVisual(_root); + + if (m.HasValue) + { + var bounds = new Rect(visual.Bounds.Size).TransformToAABB(m.Value); + + //use transformedbounds as previous render state of the visual bounds + //so we can invalidate old and new bounds of a control in case it moved/shrinked + if (visual.TransformedBounds.HasValue) + { + var trb = visual.TransformedBounds.Value; + var trBounds = trb.Bounds.TransformToAABB(trb.Transform); + + if (trBounds != bounds) + { + _renderRoot?.Invalidate(trBounds); + } + } + + _renderRoot?.Invalidate(bounds); + } + } + } + + /// + /// Ends the operation of the renderer. + /// + public void Dispose() + { + _renderTarget?.Dispose(); + } + + /// + public IEnumerable HitTest(Point p, IVisual root, Func filter) + { + return HitTest(root, p, filter); + } + + public IVisual? HitTestFirst(Point p, IVisual root, Func filter) + { + return HitTest(root, p, filter).FirstOrDefault(); + } + + /// + public void RecalculateChildren(IVisual visual) => AddDirty(visual); + + /// + public void Start() + { + } + + /// + public void Stop() + { + } + + /// + Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) + { + (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); + return brush.Visual?.Bounds.Size ?? Size.Empty; + } + + /// + void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) + { + var visual = brush.Visual; + Render(new DrawingContext(context), visual, visual.Bounds); + } + + internal static void Render(IVisual visual, DrawingContext context, bool updateTransformedBounds) + { + using var renderer = new ImmediateRenderer(visual, updateTransformedBounds); + renderer.Render(context, visual, visual.Bounds); + } + + private static void ClearTransformedBounds(IVisual visual) + { + foreach (var e in visual.GetSelfAndVisualDescendants()) + { + visual.TransformedBounds = null; + } + } + + private static Rect GetTransformedBounds(IVisual visual) + { + if (visual.RenderTransform == null) + { + return visual.Bounds; + } + else + { + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var offset = Matrix.CreateTranslation(visual.Bounds.Position + origin); + var m = (-offset) * visual.RenderTransform.Value * (offset); + return visual.Bounds.TransformToAABB(m); + } + } + + private static IEnumerable HitTest( + IVisual visual, + Point p, + Func? filter) + { + _ = visual ?? throw new ArgumentNullException(nameof(visual)); + + if (filter?.Invoke(visual) != false) + { + bool containsPoint; + + if (visual is ICustomSimpleHitTest custom) + { + containsPoint = custom.HitTest(p); + } + else + { + containsPoint = visual.TransformedBounds?.Contains(p) == true; + } + + if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Count > 0) + { + foreach (var child in visual.VisualChildren.SortByZIndex()) + { + foreach (var result in HitTest(child, p, filter)) + { + yield return result; + } + } + } + + if (containsPoint) + { + yield return visual; + } + } + } + + private void Render(DrawingContext context, IVisual visual, Rect clipRect) + { + var opacity = visual.Opacity; + var clipToBounds = visual.ClipToBounds; + var bounds = new Rect(visual.Bounds.Size); + + if (visual.IsVisible && opacity > 0) + { + var m = Matrix.CreateTranslation(visual.Bounds.Position); + + var renderTransform = Matrix.Identity; + + if (visual.RenderTransform != null) + { + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var offset = Matrix.CreateTranslation(origin); + renderTransform = (-offset) * visual.RenderTransform.Value * (offset); + } + + if (visual.HasMirrorTransform) + { + var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); + renderTransform *= mirrorMatrix; + } + + m = renderTransform * m; + + if (clipToBounds) + { + if (visual.RenderTransform != null) + { + clipRect = new Rect(visual.Bounds.Size); + } + else + { + clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size)); + } + } + + using (context.PushPostTransform(m)) + using (context.PushOpacity(opacity)) + using (clipToBounds +#pragma warning disable CS0618 // Type or member is obsolete + ? visual is IVisualWithRoundRectClip roundClipVisual + ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) + : context.PushClip(bounds) + : default(DrawingContext.PushedState)) +#pragma warning restore CS0618 // Type or member is obsolete + + using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) + using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState)) + using (context.PushTransformContainer()) + { + visual.Render(context); + +#pragma warning disable 0618 + var transformed = + new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform); +#pragma warning restore 0618 + + if (_updateTransformedBounds) + visual.TransformedBounds = transformed; + + foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)) + { + var childBounds = GetTransformedBounds(child); + + if (!child.ClipToBounds || clipRect.Intersects(childBounds)) + { + var childClipRect = child.RenderTransform == null + ? clipRect.Translate(-childBounds.Position) + : clipRect; + Render(context, child, childClipRect); + } + else if (_updateTransformedBounds) + { + ClearTransformedBounds(child); + } + } + } + } + + if (!visual.IsVisible && _updateTransformedBounds) + { + ClearTransformedBounds(visual); + } + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Base/Rendering/ManagedDeferredRendererLock.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs rename to src/Avalonia.Base/Rendering/ManagedDeferredRendererLock.cs diff --git a/src/Avalonia.Visuals/Rendering/RenderLayer.cs b/src/Avalonia.Base/Rendering/RenderLayer.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/RenderLayer.cs rename to src/Avalonia.Base/Rendering/RenderLayer.cs diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Base/Rendering/RenderLayers.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/RenderLayers.cs rename to src/Avalonia.Base/Rendering/RenderLayers.cs diff --git a/src/Avalonia.Visuals/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/RenderLoop.cs rename to src/Avalonia.Base/Rendering/RenderLoop.cs diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Base/Rendering/RendererBase.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/RendererBase.cs rename to src/Avalonia.Base/Rendering/RendererBase.cs diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs new file mode 100644 index 0000000000..98e89f6549 --- /dev/null +++ b/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs @@ -0,0 +1,68 @@ +using Avalonia.Platform; +using Avalonia.Media.Imaging; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A node in the scene graph which represents an bitmap blending mode push or pop. + /// + internal class BitmapBlendModeNode : IDrawOperation + { + /// + /// Initializes a new instance of the class that represents an + /// push. + /// + /// The to push. + public BitmapBlendModeNode(BitmapBlendingMode bitmapBlend) + { + BlendingMode = bitmapBlend; + } + + /// + /// Initializes a new instance of the class that represents an + /// pop. + /// + public BitmapBlendModeNode() + { + } + + /// + public Rect Bounds => Rect.Empty; + + /// + /// Gets the BitmapBlend to be pushed or null if the operation represents a pop. + /// + public BitmapBlendingMode? BlendingMode { get; } + + /// + public bool HitTest(Point p) => false; + + /// + /// Determines if this draw operation equals another. + /// + /// the how to compare + /// True if the draw operations are the same, otherwise false. + /// + /// The properties of the other draw operation are passed in as arguments to prevent + /// allocation of a not-yet-constructed draw operation object. + /// + public bool Equals(BitmapBlendingMode? blendingMode) => BlendingMode == blendingMode; + + /// + public void Render(IDrawingContextImpl context) + { + if (BlendingMode.HasValue) + { + context.PushBitmapBlendMode(BlendingMode.Value); + } + else + { + context.PopBitmapBlendMode(); + } + } + + public void Dispose() + { + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs rename to src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs rename to src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs new file mode 100644 index 0000000000..5225b85020 --- /dev/null +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -0,0 +1,479 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Utilities; +using Avalonia.Media.Imaging; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A drawing context which builds a scene graph. + /// + internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport + { + private readonly ISceneBuilder _sceneBuilder; + private VisualNode? _node; + private int _childIndex; + private int _drawOperationindex; + + /// + /// Initializes a new instance of the class. + /// + /// + /// A scene builder used for constructing child scenes for visual brushes. + /// + /// The scene layers. + public DeferredDrawingContextImpl(ISceneBuilder sceneBuilder, SceneLayers layers) + { + _sceneBuilder = sceneBuilder; + Layers = layers; + } + + /// + public Matrix Transform { get; set; } = Matrix.Identity; + + /// + /// Gets the layers in the scene being built. + /// + public SceneLayers Layers { get; } + + /// + /// Informs the drawing context of the visual node that is about to be rendered. + /// + /// The visual node. + /// + /// An object which when disposed will commit the changes to visual node. + /// + public UpdateState BeginUpdate(VisualNode node) + { + _ = node ?? throw new ArgumentNullException(nameof(node)); + + if (_node != null) + { + if (_childIndex < _node.Children.Count) + { + _node.ReplaceChild(_childIndex, node); + } + else + { + _node.AddChild(node); + } + + ++_childIndex; + } + + var state = new UpdateState(this, _node, _childIndex, _drawOperationindex); + _node = node; + _childIndex = _drawOperationindex = 0; + return state; + } + + /// + public void Clear(Color color) + { + // Cannot clear a deferred scene. + } + + /// + public void Dispose() + { + // Nothing to do here since we allocate no unmanaged resources. + } + + /// + /// Removes any remaining drawing operations from the visual node. + /// + /// + /// Drawing operations are updated in place, overwriting existing drawing operations if + /// they are different. Once drawing has completed for the current visual node, it is + /// possible that there are stale drawing operations at the end of the list. This method + /// trims these stale drawing operations. + /// + public void TrimChildren() + { + _node!.TrimChildren(_childIndex); + } + + /// + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) + { + Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) + { + Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) + { + // This method is currently only used to composite layers so shouldn't be called here. + throw new NotSupportedException(); + } + + /// + public void DrawLine(IPen pen, Point p1, Point p2) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) + { + Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, + BoxShadows boxShadows = default) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) + { + Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, material, rect)) + { + Add(new ExperimentalAcrylicNode(Transform, material, rect)); + } + else + { + ++_drawOperationindex; + } + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) + { + Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); + } + else + { + ++_drawOperationindex; + } + } + + public void Custom(ICustomDrawOperation custom) + { + var next = NextDrawAs(); + if (next == null || !next.Item.Equals(Transform, custom)) + Add(new CustomDrawOperation(custom, Transform)); + else + ++_drawOperationindex; + } + + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) + { + Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); + } + + else + { + ++_drawOperationindex; + } + } + public IDrawingContextLayerImpl CreateLayer(Size size) + { + throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); + } + + /// + public void PopClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new ClipNode()); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void PopGeometryClip() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new GeometryClipNode()); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void PopBitmapBlendMode() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new BitmapBlendModeNode()); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void PopOpacity() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new OpacityNode()); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void PopOpacityMask() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null, null)) + { + Add(new OpacityMaskNode()); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void PushClip(Rect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void PushClip(RoundedRect clip) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new ClipNode(Transform, clip)); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void PushGeometryClip(IGeometryImpl? clip) + { + if (clip is null) + return; + + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, clip)) + { + Add(new GeometryClipNode(Transform, clip)); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void PushOpacity(double opacity) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(opacity)) + { + Add(new OpacityNode(opacity)); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void PushOpacityMask(IBrush mask, Rect bounds) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(mask, bounds)) + { + Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); + } + else + { + ++_drawOperationindex; + } + } + + /// + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(blendingMode)) + { + Add(new BitmapBlendModeNode(blendingMode)); + } + else + { + ++_drawOperationindex; + } + } + + public readonly struct UpdateState : IDisposable + { + public UpdateState( + DeferredDrawingContextImpl owner, + VisualNode? node, + int childIndex, + int drawOperationIndex) + { + Owner = owner; + Node = node; + ChildIndex = childIndex; + DrawOperationIndex = drawOperationIndex; + } + + public void Dispose() + { + Owner._node!.TrimDrawOperations(Owner._drawOperationindex); + + var dirty = Owner.Layers.GetOrAdd(Owner._node.LayerRoot!).Dirty; + + var drawOperations = Owner._node.DrawOperations; + var drawOperationsCount = drawOperations.Count; + + for (var i = 0; i < drawOperationsCount; i++) + { + dirty.Add(drawOperations[i].Item.Bounds); + } + + Owner._node = Node; + Owner._childIndex = ChildIndex; + Owner._drawOperationindex = DrawOperationIndex; + } + + public DeferredDrawingContextImpl Owner { get; } + public VisualNode? Node { get; } + public int ChildIndex { get; } + public int DrawOperationIndex { get; } + } + + private void Add(T node) where T : class, IDrawOperation + { + using (var refCounted = RefCountable.Create(node)) + { + Add(refCounted); + } + } + + private void Add(IRef node) + { + if (_drawOperationindex < _node!.DrawOperations.Count) + { + _node.ReplaceDrawOperation(_drawOperationindex, node); + } + else + { + _node.AddDrawOperation(node); + } + + ++_drawOperationindex; + } + + private IRef? NextDrawAs() where T : class, IDrawOperation + { + return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef : null; + } + + private IDictionary? CreateChildScene(IBrush? brush) + { + var visualBrush = brush as VisualBrush; + + if (visualBrush != null) + { + var visual = visualBrush.Visual; + + if (visual != null) + { + (visual as IVisualBrushInitialize)?.EnsureInitialized(); + var scene = new Scene(visual); + _sceneBuilder.UpdateAll(scene); + return new Dictionary { { visualBrush.Visual, scene } }; + } + } + + return null; + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs rename to src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryBoundsHelper.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/GeometryBoundsHelper.cs rename to src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs rename to src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/ISceneBuilder.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/ISceneBuilder.cs rename to src/Avalonia.Base/Rendering/SceneGraph/ISceneBuilder.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/IVisualNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/IVisualNode.cs diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs new file mode 100644 index 0000000000..23267166a5 --- /dev/null +++ b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs @@ -0,0 +1,119 @@ +using Avalonia.Platform; +using Avalonia.Utilities; +using Avalonia.Media.Imaging; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A node in the scene graph which represents an image draw. + /// + internal class ImageNode : DrawOperation + { + /// + /// Initializes a new instance of the class. + /// + /// The transform. + /// The image to draw. + /// The draw opacity. + /// The source rect. + /// The destination rect. + /// The bitmap interpolation mode. + public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + : base(destRect, transform) + { + Transform = transform; + Source = source.Clone(); + Opacity = opacity; + SourceRect = sourceRect; + DestRect = destRect; + BitmapInterpolationMode = bitmapInterpolationMode; + SourceVersion = Source.Item.Version; + } + + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + + /// + /// Gets the image to draw. + /// + public IRef Source { get; } + + /// + /// Source bitmap Version + /// + public int SourceVersion { get; } + + /// + /// Gets the draw opacity. + /// + public double Opacity { get; } + + /// + /// Gets the source rect. + /// + public Rect SourceRect { get; } + + /// + /// Gets the destination rect. + /// + public Rect DestRect { get; } + + /// + /// Gets the bitmap interpolation mode. + /// + /// + /// The scaling mode. + /// + public BitmapInterpolationMode BitmapInterpolationMode { get; } + + /// + /// The bitmap blending mode. + /// + /// + /// The blending mode. + /// + public BitmapBlendingMode BitmapBlendingMode { get; } + + /// + /// Determines if this draw operation equals another. + /// + /// The transform of the other draw operation. + /// The image of the other draw operation. + /// The opacity of the other draw operation. + /// The source rect of the other draw operation. + /// The dest rect of the other draw operation. + /// The bitmap interpolation mode. + /// True if the draw operations are the same, otherwise false. + /// + /// The properties of the other draw operation are passed in as arguments to prevent + /// allocation of a not-yet-constructed draw operation object. + /// + public bool Equals(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + { + return transform == Transform && + Equals(source.Item, Source.Item) && + source.Item.Version == SourceVersion && + opacity == Opacity && + sourceRect == SourceRect && + destRect == DestRect && + bitmapInterpolationMode == BitmapInterpolationMode; + } + + /// + public override void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); + } + + /// + public override bool HitTest(Point p) => Bounds.Contains(p); + + public override void Dispose() + { + Source?.Dispose(); + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineBoundsHelper.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs rename to src/Avalonia.Base/Rendering/SceneGraph/LineBoundsHelper.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/OpacityNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs rename to src/Avalonia.Base/Rendering/SceneGraph/Scene.cs diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs new file mode 100644 index 0000000000..019c3e0e9b --- /dev/null +++ b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// Builds a scene graph from a visual tree. + /// + public class SceneBuilder : ISceneBuilder + { + /// + public void UpdateAll(Scene scene) + { + _ = scene ?? throw new ArgumentNullException(nameof(scene)); + Dispatcher.UIThread.VerifyAccess(); + + UpdateSize(scene); + scene.Layers.GetOrAdd(scene.Root.Visual); + + using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) + using (var context = new DrawingContext(impl)) + { + var clip = new Rect(scene.Root.Visual.Bounds.Size); + Update(context, scene, (VisualNode)scene.Root, clip, true); + } + } + + /// + public bool Update(Scene scene, IVisual visual) + { + _ = scene ?? throw new ArgumentNullException(nameof(scene)); + _ = visual ?? throw new ArgumentNullException(nameof(visual)); + + Dispatcher.UIThread.VerifyAccess(); + + if (!scene.Root.Visual.IsVisible) + { + throw new AvaloniaInternalException("Cannot update the scene for an invisible root visual."); + } + + var node = (VisualNode?)scene.FindNode(visual); + + if (visual == scene.Root.Visual) + { + UpdateSize(scene); + } + + if (visual.VisualRoot == scene.Root.Visual) + { + if (node?.Parent != null && + visual.VisualParent != null && + node.Parent.Visual != visual.VisualParent) + { + // The control has changed parents. Remove the node and recurse into the new parent node. + ((VisualNode)node.Parent).RemoveChild(node); + Deindex(scene, node); + node = (VisualNode?)scene.FindNode(visual.VisualParent); + } + + if (visual.IsVisible) + { + // If the node isn't yet part of the scene, find the nearest ancestor that is. + node = node ?? FindExistingAncestor(scene, visual); + + // We don't need to do anything if this part of the tree has already been fully + // updated. + if (node != null && !node.SubTreeUpdated) + { + // If the control we've been asked to update isn't part of the scene then + // we're carrying out an add operation, so recurse and add all the + // descendents too. + var recurse = node.Visual != visual; + + using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) + using (var context = new DrawingContext(impl)) + { + var clip = new Rect(scene.Root.Visual.Bounds.Size); + + if (node.Parent != null) + { + context.PushPostTransform(node.Parent.Transform); + clip = node.Parent.ClipBounds; + } + + using (context.PushTransformContainer()) + { + Update(context, scene, node, clip, recurse); + } + } + + return true; + } + } + else + { + if (node != null) + { + // The control has been hidden so remove it from its parent and deindex the + // node and its descendents. + ((VisualNode?)node.Parent)?.RemoveChild(node); + Deindex(scene, node); + return true; + } + } + } + else if (node != null) + { + // The control has been removed so remove it from its parent and deindex the + // node and its descendents. + var trim = FindFirstDeadAncestor(scene, node); + ((VisualNode)trim.Parent!).RemoveChild(trim); + Deindex(scene, trim); + return true; + } + + return false; + } + + private static VisualNode? FindExistingAncestor(Scene scene, IVisual visual) + { + var node = scene.FindNode(visual); + + while (node == null && visual.IsVisible) + { + var parent = visual.VisualParent; + + if (parent is null) + return null; + + visual = parent; + node = scene.FindNode(visual); + } + + return visual.IsVisible ? (VisualNode?)node : null; + } + + private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node) + { + var parent = node.Parent; + + while (parent!.Visual.VisualRoot == null) + { + node = parent; + parent = node.Parent; + } + + return (VisualNode)node; + } + + private static object GetOrCreateChildNode(Scene scene, IVisual child, VisualNode parent) + { + var result = (VisualNode?)scene.FindNode(child); + + if (result != null && result.Parent != parent) + { + Deindex(scene, result); + result = null; + } + + return result ?? CreateNode(scene, child, parent); + } + + private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse) + { + var visual = node.Visual; + var opacity = visual.Opacity; + var clipToBounds = visual.ClipToBounds; +#pragma warning disable CS0618 // Type or member is obsolete + var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ? + roundRectClip.ClipToBoundsRadius : + default; +#pragma warning restore CS0618 // Type or member is obsolete + + var bounds = new Rect(visual.Bounds.Size); + var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; + + contextImpl.Layers.Find(node.LayerRoot!)?.Dirty.Add(node.Bounds); + + if (visual.IsVisible) + { + var m = node != scene.Root ? + Matrix.CreateTranslation(visual.Bounds.Position) : + Matrix.Identity; + + var renderTransform = Matrix.Identity; + + if (visual.RenderTransform != null) + { + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var offset = Matrix.CreateTranslation(origin); + renderTransform = (-offset) * visual.RenderTransform.Value * (offset); + } + + if (visual.HasMirrorTransform) + { + var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); + renderTransform *= mirrorMatrix; + } + + m = renderTransform * m; + + using (contextImpl.BeginUpdate(node)) + using (context.PushPostTransform(m)) + using (context.PushTransformContainer()) + { + var globalBounds = bounds.TransformToAABB(contextImpl.Transform); + var clipBounds = clipToBounds ? + globalBounds.Intersect(clip) : + clip; + + forceRecurse = forceRecurse || + node.ClipBounds != clipBounds || + node.Opacity != opacity || + node.Transform != contextImpl.Transform; + + node.Transform = contextImpl.Transform; + node.ClipBounds = clipBounds; + node.ClipToBounds = clipToBounds; + node.LayoutBounds = globalBounds; + node.ClipToBoundsRadius = clipToBoundsRadius; + node.GeometryClip = visual.Clip?.PlatformImpl; + node.Opacity = opacity; + + // TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning. + node.OpacityMask = visual.OpacityMask?.ToImmutable(); + + if (ShouldStartLayer(visual)) + { + if (node.LayerRoot != visual) + { + MakeLayer(scene, node); + } + else + { + UpdateLayer(node, scene.Layers[node.LayerRoot]); + } + } + else if (node.LayerRoot == node.Visual && node.Parent != null) + { + ClearLayer(scene, node); + } + + if (node.ClipToBounds) + { + clip = clip.Intersect(node.ClipBounds); + } + + try + { + visual.Render(context); + } + catch { } + + var transformed = new TransformedBounds(new Rect(visual.Bounds.Size), clip, node.Transform); + visual.TransformedBounds = transformed; + + if (forceRecurse) + { + var visualChildren = (IList) visual.VisualChildren; + + node.TryPreallocateChildren(visualChildren.Count); + + if (visualChildren.Count == 1) + { + var childNode = GetOrCreateChildNode(scene, visualChildren[0], node); + Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + } + else if (visualChildren.Count > 1) + { + var count = visualChildren.Count; + var sortedChildren = new (IVisual visual, int index)[count]; + + for (var i = 0; i < count; i++) + { + sortedChildren[i] = (visualChildren[i], i); + } + + // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. + Array.Sort(sortedChildren, (lhs, rhs) => + { + var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); + + return result == 0 ? lhs.index.CompareTo(rhs.index) : result; + }); + + foreach (var child in sortedChildren) + { + var childNode = GetOrCreateChildNode(scene, child.Item1, node); + Update(context, scene, (VisualNode)childNode, clip, forceRecurse); + } + } + + node.SubTreeUpdated = true; + contextImpl.TrimChildren(); + } + } + } + else + { + contextImpl.BeginUpdate(node).Dispose(); + } + } + + private void UpdateSize(Scene scene) + { + var renderRoot = scene.Root.Visual as IRenderRoot; + var newSize = renderRoot?.ClientSize ?? scene.Root.Visual.Bounds.Size; + + scene.Scaling = renderRoot?.RenderScaling ?? 1; + + if (scene.Size != newSize) + { + var oldSize = scene.Size; + + scene.Size = newSize; + + Rect horizontalDirtyRect = Rect.Empty; + Rect verticalDirtyRect = Rect.Empty; + + if (newSize.Width > oldSize.Width) + { + horizontalDirtyRect = new Rect(oldSize.Width, 0, newSize.Width - oldSize.Width, oldSize.Height); + } + + if (newSize.Height > oldSize.Height) + { + verticalDirtyRect = new Rect(0, oldSize.Height, newSize.Width, newSize.Height - oldSize.Height); + } + + foreach (var layer in scene.Layers) + { + layer.Dirty.Add(horizontalDirtyRect); + layer.Dirty.Add(verticalDirtyRect); + } + } + } + + private static VisualNode CreateNode(Scene scene, IVisual visual, VisualNode parent) + { + var node = new VisualNode(visual, parent); + node.LayerRoot = parent.LayerRoot; + scene.Add(node); + return node; + } + + private static void Deindex(Scene scene, VisualNode node) + { + var nodeChildren = node.Children; + var nodeChildrenCount = nodeChildren.Count; + + for (var i = 0; i < nodeChildrenCount; i++) + { + if (nodeChildren[i] is VisualNode visual) + { + Deindex(scene, visual); + } + } + + scene.Remove(node); + + node.SubTreeUpdated = true; + + scene.Layers[node.LayerRoot!].Dirty.Add(node.Bounds); + + node.Visual.TransformedBounds = null; + + if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual) + { + scene.Layers.Remove(node.LayerRoot); + } + } + + private static void ClearLayer(Scene scene, VisualNode node) + { + var parent = (VisualNode)node.Parent!; + var oldLayerRoot = node.LayerRoot; + var newLayerRoot = parent.LayerRoot!; + var existingDirtyRects = scene.Layers[node.LayerRoot!].Dirty; + var newDirtyRects = scene.Layers[newLayerRoot].Dirty; + + existingDirtyRects.Coalesce(); + + foreach (var r in existingDirtyRects) + { + newDirtyRects.Add(r); + } + + var oldLayer = scene.Layers[oldLayerRoot!]; + PropagateLayer(node, scene.Layers[newLayerRoot], oldLayer); + scene.Layers.Remove(oldLayer); + } + + private static void MakeLayer(Scene scene, VisualNode node) + { + var oldLayerRoot = node.LayerRoot!; + var layer = scene.Layers.Add(node.Visual); + var oldLayer = scene.Layers[oldLayerRoot!]; + + UpdateLayer(node, layer); + PropagateLayer(node, layer, scene.Layers[oldLayerRoot]); + } + + private static void UpdateLayer(VisualNode node, SceneLayer layer) + { + layer.Opacity = node.Visual.Opacity; + + if (node.Visual.OpacityMask != null) + { + layer.OpacityMask = node.Visual.OpacityMask?.ToImmutable(); + layer.OpacityMaskRect = node.ClipBounds; + } + else + { + layer.OpacityMask = null; + layer.OpacityMaskRect = Rect.Empty; + } + + layer.GeometryClip = node.HasAncestorGeometryClip ? + CreateLayerGeometryClip(node) : + null; + } + + private static void PropagateLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer) + { + node.LayerRoot = layer.LayerRoot; + + layer.Dirty.Add(node.Bounds); + oldLayer.Dirty.Add(node.Bounds); + + foreach (VisualNode child in node.Children) + { + // If the child is not the start of a new layer, recurse. + if (child.LayerRoot != child.Visual) + { + PropagateLayer(child, layer, oldLayer); + } + } + } + + // HACK: Disabled layers because they're broken in current renderer. See #2244. + private static bool ShouldStartLayer(IVisual visual) => false; + + private static IGeometryImpl? CreateLayerGeometryClip(VisualNode node) + { + IGeometryImpl? result = null; + VisualNode? n = node; + + for (;;) + { + n = (VisualNode?)n!.Parent; + + if (n == null || (n.GeometryClip == null && !n.HasAncestorGeometryClip)) + { + break; + } + + if (n?.GeometryClip != null) + { + var transformed = n.GeometryClip.WithTransform(n.Transform); + + result = result == null ? transformed : result.Intersect(transformed); + } + } + + return result; + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayer.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneLayer.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayer.cs rename to src/Avalonia.Base/Rendering/SceneGraph/SceneLayer.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneLayers.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs rename to src/Avalonia.Base/Rendering/SceneGraph/SceneLayers.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs rename to src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneInvalidatedEventArgs.cs b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SceneInvalidatedEventArgs.cs rename to src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs diff --git a/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs rename to src/Avalonia.Base/Rendering/SleepLoopRenderTimer.cs diff --git a/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs b/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs rename to src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs diff --git a/src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs b/src/Avalonia.Base/Rendering/Utilities/TileBrushCalculator.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/Utilities/TileBrushCalculator.cs rename to src/Avalonia.Base/Rendering/Utilities/TileBrushCalculator.cs diff --git a/src/Avalonia.Visuals/Rendering/ZIndexComparer.cs b/src/Avalonia.Base/Rendering/ZIndexComparer.cs similarity index 100% rename from src/Avalonia.Visuals/Rendering/ZIndexComparer.cs rename to src/Avalonia.Base/Rendering/ZIndexComparer.cs diff --git a/src/Avalonia.Visuals/RoundedRect.cs b/src/Avalonia.Base/RoundedRect.cs similarity index 100% rename from src/Avalonia.Visuals/RoundedRect.cs rename to src/Avalonia.Base/RoundedRect.cs diff --git a/src/Avalonia.Base/Settings.StyleCop b/src/Avalonia.Base/Settings.StyleCop deleted file mode 100644 index bb05f99bc1..0000000000 --- a/src/Avalonia.Base/Settings.StyleCop +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Base/Size.cs similarity index 100% rename from src/Avalonia.Visuals/Size.cs rename to src/Avalonia.Base/Size.cs diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs new file mode 100644 index 0000000000..f98d2cdbcc --- /dev/null +++ b/src/Avalonia.Base/StyledElement.cs @@ -0,0 +1,893 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using Avalonia.Animation; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Diagnostics; +using Avalonia.Logging; +using Avalonia.LogicalTree; +using Avalonia.Styling; + +#nullable enable + +namespace Avalonia +{ + /// + /// Extends an with the following features: + /// + /// - An inherited . + /// - Implements to allow styling to work on the styled element. + /// - Implements to form part of a logical tree. + /// - A collection of class strings for custom styling. + /// + public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent + { + /// + /// Defines the property. + /// + public static readonly StyledProperty DataContextProperty = + AvaloniaProperty.Register( + nameof(DataContext), + inherits: true, + notifying: DataContextNotifying); + + /// + /// Defines the property. + /// + public static readonly DirectProperty NameProperty = + AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ParentProperty = + AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); + + /// + /// Defines the property. + /// + public static readonly DirectProperty TemplatedParentProperty = + AvaloniaProperty.RegisterDirect( + nameof(TemplatedParent), + o => o.TemplatedParent, + (o ,v) => o.TemplatedParent = v); + + private int _initCount; + private string? _name; + private readonly Classes _classes = new Classes(); + private ILogicalRoot? _logicalRoot; + private IAvaloniaList? _logicalChildren; + private IResourceDictionary? _resources; + private Styles? _styles; + private bool _styled; + private List? _appliedStyles; + private ITemplatedControl? _templatedParent; + private bool _dataContextUpdating; + + /// + /// Initializes static members of the class. + /// + static StyledElement() + { + DataContextProperty.Changed.AddClassHandler((x,e) => x.OnDataContextChangedCore(e)); + } + + /// + /// Initializes a new instance of the class. + /// + public StyledElement() + { + _logicalRoot = this as ILogicalRoot; + } + + /// + /// Raised when the styled element is attached to a rooted logical tree. + /// + public event EventHandler? AttachedToLogicalTree; + + /// + /// Raised when the styled element is detached from a rooted logical tree. + /// + public event EventHandler? DetachedFromLogicalTree; + + /// + /// Occurs when the property changes. + /// + /// + /// This event will be raised when the property has changed and + /// all subscribers to that change have been notified. + /// + public event EventHandler? DataContextChanged; + + /// + /// Occurs when the styled element has finished initialization. + /// + /// + /// The Initialized event indicates that all property values on the styled element have been set. + /// When loading the styled element from markup, it occurs when + /// is called *and* the styled element + /// is attached to a rooted logical tree. When the styled element is created by code and + /// is not used, it is called when the styled element is attached + /// to the visual tree. + /// + public event EventHandler? Initialized; + + /// + /// Occurs when a resource in this styled element or a parent styled element has changed. + /// + public event EventHandler? ResourcesChanged; + + /// + /// Gets or sets the name of the styled element. + /// + /// + /// An element's name is used to uniquely identify an element within the element's name + /// scope. Once the element is added to a logical tree, its name cannot be changed. + /// + public string? Name + { + get => _name; + + set + { + if (_styled) + { + throw new InvalidOperationException("Cannot set Name : styled element already styled."); + } + + _name = value; + } + } + + /// + /// Gets or sets the styled element's classes. + /// + /// + /// + /// Classes can be used to apply user-defined styling to styled elements, or to allow styled elements + /// that share a common purpose to be easily selected. + /// + /// + /// Even though this property can be set, the setter is only intended for use in object + /// initializers. Assigning to this property does not change the underlying collection, + /// it simply clears the existing collection and adds the contents of the assigned + /// collection. + /// + /// + public Classes Classes + { + get + { + return _classes; + } + + set + { + if (_classes != value) + { + _classes.Replace(value); + } + } + } + + /// + /// Gets or sets the control's data context. + /// + /// + /// The data context is an inherited property that specifies the default object that will + /// be used for data binding. + /// + public object? DataContext + { + get { return GetValue(DataContextProperty); } + set { SetValue(DataContextProperty, value); } + } + + /// + /// Gets a value that indicates whether the element has finished initialization. + /// + /// + /// For more information about when IsInitialized is set, see the + /// event. + /// + public bool IsInitialized { get; private set; } + + /// + /// Gets the styles for the styled element. + /// + /// + /// Styles for the entire application are added to the Application.Styles collection, but + /// each styled element may in addition define its own styles which are applied to the styled element + /// itself and its children. + /// + public Styles Styles => _styles ??= new Styles(this); + + /// + /// Gets or sets the styled element's resource dictionary. + /// + public IResourceDictionary Resources + { + get => _resources ??= new ResourceDictionary(this); + set + { + value = value ?? throw new ArgumentNullException(nameof(value)); + _resources?.RemoveOwner(this); + _resources = value; + _resources.AddOwner(this); + } + } + + /// + /// Gets the styled element whose lookless template this styled element is part of. + /// + public ITemplatedControl? TemplatedParent + { + get => _templatedParent; + internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); + } + + /// + /// Gets the styled element's logical children. + /// + protected IAvaloniaList LogicalChildren + { + get + { + if (_logicalChildren == null) + { + var list = new AvaloniaList + { + ResetBehavior = ResetBehavior.Remove, + Validate = logical => ValidateLogicalChild(logical) + }; + list.CollectionChanged += LogicalChildrenCollectionChanged; + _logicalChildren = list; + } + + return _logicalChildren; + } + } + + /// + /// Gets the collection in a form that allows adding and removing + /// pseudoclasses. + /// + protected IPseudoClasses PseudoClasses => Classes; + + /// + /// Gets a value indicating whether the element is attached to a rooted logical tree. + /// + bool ILogical.IsAttachedToLogicalTree => _logicalRoot != null; + + /// + /// Gets the styled element's logical parent. + /// + public IStyledElement? Parent { get; private set; } + + /// + /// Gets the styled element's logical parent. + /// + ILogical? ILogical.LogicalParent => Parent; + + /// + /// Gets the styled element's logical children. + /// + IAvaloniaReadOnlyList ILogical.LogicalChildren => LogicalChildren; + + /// + bool IResourceNode.HasResources => (_resources?.HasResources ?? false) || + (((IResourceNode?)_styles)?.HasResources ?? false); + + /// + IAvaloniaReadOnlyList IStyleable.Classes => Classes; + + /// + /// Gets the type by which the styled element is styled. + /// + /// + /// Usually controls are styled by their own type, but there are instances where you want + /// a styled element to be styled by its base type, e.g. creating SpecialButton that + /// derives from Button and adds extra functionality but is still styled as a regular + /// Button. + /// + Type IStyleable.StyleKey => GetType(); + + /// + bool IStyleHost.IsStylesInitialized => _styles != null; + + /// + IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; + + /// + public virtual void BeginInit() + { + ++_initCount; + } + + /// + public virtual void EndInit() + { + if (_initCount == 0) + { + throw new InvalidOperationException("BeginInit was not called."); + } + + if (--_initCount == 0 && _logicalRoot != null) + { + ApplyStyling(); + InitializeIfNeeded(); + } + } + + /// + /// Applies styling to the control if the control is initialized and styling is not + /// already applied. + /// + /// + /// A value indicating whether styling is now applied to the control. + /// + protected bool ApplyStyling() + { + if (_initCount == 0 && !_styled) + { + try + { + BeginBatchUpdate(); + AvaloniaLocator.Current.GetService()?.ApplyStyles(this); + } + finally + { + EndBatchUpdate(); + } + + _styled = true; + } + + return _styled; + } + + /// + /// Detaches all styles from the element and queues a restyle. + /// + protected virtual void InvalidateStyles() => DetachStyles(); + + protected void InitializeIfNeeded() + { + if (_initCount == 0 && !IsInitialized) + { + IsInitialized = true; + OnInitialized(); + Initialized?.Invoke(this, EventArgs.Empty); + } + } + + internal StyleDiagnostics GetStyleDiagnosticsInternal() + { + IReadOnlyList? appliedStyles = _appliedStyles; + + if (appliedStyles is null) + { + appliedStyles = Array.Empty(); + } + + return new StyleDiagnostics(appliedStyles); + } + + /// + void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + OnAttachedToLogicalTreeCore(e); + } + + /// + void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + OnDetachedFromLogicalTreeCore(e); + } + + /// + void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); + + /// + void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); + + /// + bool IResourceNode.TryGetResource(object key, out object? value) + { + value = null; + return (_resources?.TryGetResource(key, out value) ?? false) || + (_styles?.TryGetResource(key, out value) ?? false); + } + + /// + /// Sets the styled element's logical parent. + /// + /// The parent. + void ISetLogicalParent.SetParent(ILogical? parent) + { + var old = Parent; + + if (parent != old) + { + if (old != null && parent != null) + { + throw new InvalidOperationException("The Control already has a parent."); + } + + if (InheritanceParent == null || parent == null) + { + InheritanceParent = parent as AvaloniaObject; + } + + Parent = (IStyledElement?)parent; + + if (_logicalRoot != null) + { + var e = new LogicalTreeAttachmentEventArgs(_logicalRoot, this, old!); + OnDetachedFromLogicalTreeCore(e); + } + + var newRoot = FindLogicalRoot(this); + + if (newRoot is object) + { + var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent!); + OnAttachedToLogicalTreeCore(e); + } + else if (parent is null) + { + // If we were attached to the logical tree, we piggyback on the tree traversal + // there to raise resources changed notifications. If we're being removed from + // the logical tree, then traverse the tree raising notifications now. + // + // We don't raise resources changed notifications if we're being attached to a + // non-rooted control beacuse it's unlikely that dynamic resources need to be + // correct until the control is added to the tree, and it causes a *lot* of + // notifications. + NotifyResourcesChanged(); + } + +#nullable disable + RaisePropertyChanged( + ParentProperty, + new Optional(old), + new BindingValue(Parent), + BindingPriority.LocalValue); +#nullable enable + } + } + + /// + /// Sets the styled element's inheritance parent. + /// + /// The parent. + void ISetInheritanceParent.SetParent(IAvaloniaObject? parent) + { + InheritanceParent = parent switch + { + AvaloniaObject ao => ao, + null => null, + _ => throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.") + }; + } + + void IStyleable.StyleApplied(IStyleInstance instance) + { + instance = instance ?? throw new ArgumentNullException(nameof(instance)); + + _appliedStyles ??= new List(); + _appliedStyles.Add(instance); + } + + void IStyleable.DetachStyles() => DetachStyles(); + + void IStyleable.DetachStyles(IReadOnlyList styles) => DetachStyles(styles); + + void IStyleable.InvalidateStyles() => InvalidateStyles(); + + void IStyleHost.StylesAdded(IReadOnlyList styles) + { + InvalidateStylesOnThisAndDescendents(); + } + + void IStyleHost.StylesRemoved(IReadOnlyList styles) + { + var allStyles = RecurseStyles(styles); + DetachStylesFromThisAndDescendents(allStyles); + } + + protected virtual void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + SetLogicalParent(e.NewItems!); + break; + + case NotifyCollectionChangedAction.Remove: + ClearLogicalParent(e.OldItems!); + break; + + case NotifyCollectionChangedAction.Replace: + ClearLogicalParent(e.OldItems!); + SetLogicalParent(e.NewItems!); + break; + + case NotifyCollectionChangedAction.Reset: + throw new NotSupportedException("Reset should not be signaled on LogicalChildren collection"); + } + } + + /// + /// Notifies child controls that a change has been made to resources that apply to them. + /// + /// The event args. + protected virtual void NotifyChildResourcesChanged(ResourcesChangedEventArgs e) + { + if (_logicalChildren is object) + { + var count = _logicalChildren.Count; + + if (count > 0) + { + e ??= ResourcesChangedEventArgs.Empty; + + for (var i = 0; i < count; ++i) + { + _logicalChildren[i].NotifyResourcesChanged(e); + } + } + } + } + + /// + /// Called when the styled element is added to a rooted logical tree. + /// + /// The event args. + protected virtual void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + } + + /// + /// Called when the styled element is removed from a rooted logical tree. + /// + /// The event args. + protected virtual void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + } + + /// + /// Called when the property changes. + /// + /// The event args. + protected virtual void OnDataContextChanged(EventArgs e) + { + DataContextChanged?.Invoke(this, EventArgs.Empty); + } + + /// + /// Called when the begins updating. + /// + protected virtual void OnDataContextBeginUpdate() + { + } + + /// + /// Called when the finishes updating. + /// + protected virtual void OnDataContextEndUpdate() + { + } + + /// + /// Called when the control finishes initialization. + /// + protected virtual void OnInitialized() + { + } + + private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) + { + if (o is StyledElement element) + { + DataContextNotifying(element, updateStarted); + } + } + + private static void DataContextNotifying(StyledElement element, bool updateStarted) + { + if (updateStarted) + { + if (!element._dataContextUpdating) + { + element._dataContextUpdating = true; + element.OnDataContextBeginUpdate(); + + var logicalChildren = element.LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) + { + if (element.LogicalChildren[i] is StyledElement s && + s.InheritanceParent == element && + !s.IsSet(DataContextProperty)) + { + DataContextNotifying(s, updateStarted); + } + } + } + } + else + { + if (element._dataContextUpdating) + { + element.OnDataContextEndUpdate(); + element._dataContextUpdating = false; + } + } + } + + private static ILogicalRoot? FindLogicalRoot(IStyleHost? e) + { + while (e != null) + { + if (e is ILogicalRoot root) + { + return root; + } + + e = e.StylingParent; + } + + return null; + } + + private static void ValidateLogicalChild(ILogical c) + { + if (c == null) + { + throw new ArgumentException("Cannot add null to LogicalChildren."); + } + } + + private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e) + { + if (this.GetLogicalParent() == null && !(this is ILogicalRoot)) + { + throw new InvalidOperationException( + $"AttachedToLogicalTreeCore called for '{GetType().Name}' but control has no logical parent."); + } + + // This method can be called when a control is already attached to the logical tree + // in the following scenario: + // - ListBox gets assigned Items containing ListBoxItem + // - ListBox makes ListBoxItem a logical child + // - ListBox template gets applied; making its Panel get attached to logical tree + // - That AttachedToLogicalTree signal travels down to the ListBoxItem + if (_logicalRoot == null) + { + _logicalRoot = e.Root; + + ApplyStyling(); + NotifyResourcesChanged(propagate: false); + + OnAttachedToLogicalTree(e); + AttachedToLogicalTree?.Invoke(this, e); + } + + var logicalChildren = LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) + { + if (logicalChildren[i] is StyledElement child) + { + child.OnAttachedToLogicalTreeCore(e); + } + } + } + + private void OnDetachedFromLogicalTreeCore(LogicalTreeAttachmentEventArgs e) + { + if (_logicalRoot != null) + { + _logicalRoot = null; + DetachStyles(); + OnDetachedFromLogicalTree(e); + DetachedFromLogicalTree?.Invoke(this, e); + + var logicalChildren = LogicalChildren; + var logicalChildrenCount = logicalChildren.Count; + + for (var i = 0; i < logicalChildrenCount; i++) + { + if (logicalChildren[i] is StyledElement child) + { + child.OnDetachedFromLogicalTreeCore(e); + } + } + +#if DEBUG + if (((INotifyCollectionChangedDebug)Classes).GetCollectionChangedSubscribers()?.Length > 0) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log( + this, + "{Type} detached from logical tree but still has class listeners", + GetType()); + } +#endif + } + } + + private void OnDataContextChangedCore(AvaloniaPropertyChangedEventArgs e) + { + OnDataContextChanged(EventArgs.Empty); + } + + private void SetLogicalParent(IList children) + { + var count = children.Count; + + for (var i = 0; i < count; i++) + { + var logical = (ILogical) children[i]!; + + if (logical.LogicalParent is null) + { + ((ISetLogicalParent)logical).SetParent(this); + } + } + } + + private void ClearLogicalParent(IList children) + { + var count = children.Count; + + for (var i = 0; i < count; i++) + { + var logical = (ILogical) children[i]!; + + if (logical.LogicalParent == this) + { + ((ISetLogicalParent)logical).SetParent(null); + } + } + } + + private void DetachStyles() + { + if (_appliedStyles is object) + { + BeginBatchUpdate(); + + try + { + foreach (var i in _appliedStyles) + { + i.Dispose(); + } + + _appliedStyles.Clear(); + } + finally + { + EndBatchUpdate(); + } + } + + _styled = false; + } + + private void DetachStyles(IReadOnlyList styles) + { + styles = styles ?? throw new ArgumentNullException(nameof(styles)); + + if (_appliedStyles is null) + { + return; + } + + var count = styles.Count; + + for (var i = 0; i < count; ++i) + { + for (var j = _appliedStyles.Count - 1; j >= 0; --j) + { + var applied = _appliedStyles[j]; + + if (applied.Source == styles[i]) + { + applied.Dispose(); + _appliedStyles.RemoveAt(j); + } + } + } + } + + private void InvalidateStylesOnThisAndDescendents() + { + InvalidateStyles(); + + if (_logicalChildren is object) + { + var childCount = _logicalChildren.Count; + + for (var i = 0; i < childCount; ++i) + { + (_logicalChildren[i] as StyledElement)?.InvalidateStylesOnThisAndDescendents(); + } + } + } + + private void DetachStylesFromThisAndDescendents(IReadOnlyList styles) + { + DetachStyles(styles); + + if (_logicalChildren is object) + { + var childCount = _logicalChildren.Count; + + for (var i = 0; i < childCount; ++i) + { + (_logicalChildren[i] as StyledElement)?.DetachStylesFromThisAndDescendents(styles); + } + } + } + + private void NotifyResourcesChanged( + ResourcesChangedEventArgs? e = null, + bool propagate = true) + { + if (ResourcesChanged is object) + { + e ??= ResourcesChangedEventArgs.Empty; + ResourcesChanged(this, e); + } + + if (propagate) + { + e ??= ResourcesChangedEventArgs.Empty; + NotifyChildResourcesChanged(e); + } + } + + private static IReadOnlyList RecurseStyles(IReadOnlyList styles) + { + var count = styles.Count; + List? result = null; + + for (var i = 0; i < count; ++i) + { + var style = styles[i]; + + if (style.Children.Count > 0) + { + if (result is null) + { + result = new List(styles); + } + + RecurseStyles(style.Children, result); + } + } + + return result ?? styles; + } + + private static void RecurseStyles(IReadOnlyList styles, List result) + { + var count = styles.Count; + + for (var i = 0; i < count; ++i) + { + var style = styles[i]; + result.Add(style); + RecurseStyles(style.Children, result); + } + } + } +} diff --git a/src/Avalonia.Styling/StyledElementExtensions.cs b/src/Avalonia.Base/StyledElementExtensions.cs similarity index 100% rename from src/Avalonia.Styling/StyledElementExtensions.cs rename to src/Avalonia.Base/StyledElementExtensions.cs diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 535a826c1e..dd5eb703ea 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using Avalonia.Data; using Avalonia.Reactive; +using Avalonia.Styling; using Avalonia.Utilities; namespace Avalonia @@ -158,12 +159,6 @@ namespace Avalonia base.OverrideMetadata(type, metadata); } - /// - public override void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) - { - visitor.Visit(this, ref data); - } - /// /// Gets the string representation of the property. /// @@ -177,19 +172,19 @@ namespace Avalonia object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); /// - internal override void RouteClearValue(IAvaloniaObject o) + internal override void RouteClearValue(AvaloniaObject o) { o.ClearValue(this); } /// - internal override object? RouteGetValue(IAvaloniaObject o) + internal override object? RouteGetValue(AvaloniaObject o) { return o.GetValue(this); } /// - internal override object? RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority) { var value = o.GetBaseValue(this, maxPriority); return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; @@ -197,7 +192,7 @@ namespace Avalonia /// internal override IDisposable? RouteSetValue( - IAvaloniaObject o, + AvaloniaObject o, object? value, BindingPriority priority) { @@ -221,7 +216,7 @@ namespace Avalonia /// internal override IDisposable RouteBind( - IAvaloniaObject o, + AvaloniaObject o, IObservable> source, BindingPriority priority) { @@ -232,11 +227,36 @@ namespace Avalonia /// internal override void RouteInheritanceParentChanged( AvaloniaObject o, - IAvaloniaObject? oldParent) + AvaloniaObject? oldParent) { o.InheritanceParentChanged(this, oldParent); } + internal override ISetterInstance CreateSetterInstance(IStyleable target, object? value) + { + if (value is IBinding binding) + { + return new PropertySetterBindingInstance( + target, + this, + binding); + } + else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType)) + { + return new PropertySetterLazyInstance( + target, + this, + () => (TValue)template.Build()); + } + else + { + return new PropertySetterInstance( + target, + this, + (TValue)value!); + } + } + private object? GetDefaultBoxedValue(Type type) { _ = type ?? throw new ArgumentNullException(nameof(type)); diff --git a/src/Avalonia.Styling/Styling/Activators/AndActivator.cs b/src/Avalonia.Base/Styling/Activators/AndActivator.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Activators/AndActivator.cs rename to src/Avalonia.Base/Styling/Activators/AndActivator.cs diff --git a/src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs b/src/Avalonia.Base/Styling/Activators/AndActivatorBuilder.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Activators/AndActivatorBuilder.cs rename to src/Avalonia.Base/Styling/Activators/AndActivatorBuilder.cs diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs b/src/Avalonia.Base/Styling/Activators/IStyleActivator.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs rename to src/Avalonia.Base/Styling/Activators/IStyleActivator.cs diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs b/src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs rename to src/Avalonia.Base/Styling/Activators/IStyleActivatorSink.cs diff --git a/src/Avalonia.Styling/Styling/Activators/NotActivator.cs b/src/Avalonia.Base/Styling/Activators/NotActivator.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Activators/NotActivator.cs rename to src/Avalonia.Base/Styling/Activators/NotActivator.cs diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs rename to src/Avalonia.Base/Styling/Activators/NthChildActivator.cs diff --git a/src/Avalonia.Styling/Styling/Activators/OrActivator.cs b/src/Avalonia.Base/Styling/Activators/OrActivator.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Activators/OrActivator.cs rename to src/Avalonia.Base/Styling/Activators/OrActivator.cs diff --git a/src/Avalonia.Styling/Styling/Activators/OrActivatorBuilder.cs b/src/Avalonia.Base/Styling/Activators/OrActivatorBuilder.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Activators/OrActivatorBuilder.cs rename to src/Avalonia.Base/Styling/Activators/OrActivatorBuilder.cs diff --git a/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs b/src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs rename to src/Avalonia.Base/Styling/Activators/PropertyEqualsActivator.cs diff --git a/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs b/src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs rename to src/Avalonia.Base/Styling/Activators/StyleActivatorBase.cs diff --git a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs rename to src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs diff --git a/src/Avalonia.Styling/Styling/ChildSelector.cs b/src/Avalonia.Base/Styling/ChildSelector.cs similarity index 100% rename from src/Avalonia.Styling/Styling/ChildSelector.cs rename to src/Avalonia.Base/Styling/ChildSelector.cs diff --git a/src/Avalonia.Styling/Styling/DescendentSelector.cs b/src/Avalonia.Base/Styling/DescendentSelector.cs similarity index 100% rename from src/Avalonia.Styling/Styling/DescendentSelector.cs rename to src/Avalonia.Base/Styling/DescendentSelector.cs diff --git a/src/Avalonia.Styling/Styling/IGlobalStyles.cs b/src/Avalonia.Base/Styling/IGlobalStyles.cs similarity index 100% rename from src/Avalonia.Styling/Styling/IGlobalStyles.cs rename to src/Avalonia.Base/Styling/IGlobalStyles.cs diff --git a/src/Avalonia.Styling/Styling/ISetter.cs b/src/Avalonia.Base/Styling/ISetter.cs similarity index 100% rename from src/Avalonia.Styling/Styling/ISetter.cs rename to src/Avalonia.Base/Styling/ISetter.cs diff --git a/src/Avalonia.Styling/Styling/ISetterInstance.cs b/src/Avalonia.Base/Styling/ISetterInstance.cs similarity index 100% rename from src/Avalonia.Styling/Styling/ISetterInstance.cs rename to src/Avalonia.Base/Styling/ISetterInstance.cs diff --git a/src/Avalonia.Styling/Styling/ISetterValue.cs b/src/Avalonia.Base/Styling/ISetterValue.cs similarity index 100% rename from src/Avalonia.Styling/Styling/ISetterValue.cs rename to src/Avalonia.Base/Styling/ISetterValue.cs diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Base/Styling/IStyle.cs similarity index 100% rename from src/Avalonia.Styling/Styling/IStyle.cs rename to src/Avalonia.Base/Styling/IStyle.cs diff --git a/src/Avalonia.Styling/Styling/IStyleHost.cs b/src/Avalonia.Base/Styling/IStyleHost.cs similarity index 100% rename from src/Avalonia.Styling/Styling/IStyleHost.cs rename to src/Avalonia.Base/Styling/IStyleHost.cs diff --git a/src/Avalonia.Styling/Styling/IStyleInstance.cs b/src/Avalonia.Base/Styling/IStyleInstance.cs similarity index 100% rename from src/Avalonia.Styling/Styling/IStyleInstance.cs rename to src/Avalonia.Base/Styling/IStyleInstance.cs diff --git a/src/Avalonia.Styling/Styling/IStyleable.cs b/src/Avalonia.Base/Styling/IStyleable.cs similarity index 100% rename from src/Avalonia.Styling/Styling/IStyleable.cs rename to src/Avalonia.Base/Styling/IStyleable.cs diff --git a/src/Avalonia.Styling/Styling/IStyler.cs b/src/Avalonia.Base/Styling/IStyler.cs similarity index 100% rename from src/Avalonia.Styling/Styling/IStyler.cs rename to src/Avalonia.Base/Styling/IStyler.cs diff --git a/src/Avalonia.Styling/Styling/ITemplate.cs b/src/Avalonia.Base/Styling/ITemplate.cs similarity index 100% rename from src/Avalonia.Styling/Styling/ITemplate.cs rename to src/Avalonia.Base/Styling/ITemplate.cs diff --git a/src/Avalonia.Styling/Styling/ITemplatedControl.cs b/src/Avalonia.Base/Styling/ITemplatedControl.cs similarity index 100% rename from src/Avalonia.Styling/Styling/ITemplatedControl.cs rename to src/Avalonia.Base/Styling/ITemplatedControl.cs diff --git a/src/Avalonia.Styling/Styling/NotSelector.cs b/src/Avalonia.Base/Styling/NotSelector.cs similarity index 100% rename from src/Avalonia.Styling/Styling/NotSelector.cs rename to src/Avalonia.Base/Styling/NotSelector.cs diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs similarity index 100% rename from src/Avalonia.Styling/Styling/NthChildSelector.cs rename to src/Avalonia.Base/Styling/NthChildSelector.cs diff --git a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs b/src/Avalonia.Base/Styling/NthLastChildSelector.cs similarity index 100% rename from src/Avalonia.Styling/Styling/NthLastChildSelector.cs rename to src/Avalonia.Base/Styling/NthLastChildSelector.cs diff --git a/src/Avalonia.Styling/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs similarity index 100% rename from src/Avalonia.Styling/Styling/OrSelector.cs rename to src/Avalonia.Base/Styling/OrSelector.cs diff --git a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs similarity index 100% rename from src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs rename to src/Avalonia.Base/Styling/PropertyEqualsSelector.cs diff --git a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs similarity index 100% rename from src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs rename to src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs diff --git a/src/Avalonia.Styling/Styling/PropertySetterInstance.cs b/src/Avalonia.Base/Styling/PropertySetterInstance.cs similarity index 100% rename from src/Avalonia.Styling/Styling/PropertySetterInstance.cs rename to src/Avalonia.Base/Styling/PropertySetterInstance.cs diff --git a/src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs b/src/Avalonia.Base/Styling/PropertySetterLazyInstance.cs similarity index 100% rename from src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs rename to src/Avalonia.Base/Styling/PropertySetterLazyInstance.cs diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Base/Styling/Selector.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Selector.cs rename to src/Avalonia.Base/Styling/Selector.cs diff --git a/src/Avalonia.Styling/Styling/SelectorMatch.cs b/src/Avalonia.Base/Styling/SelectorMatch.cs similarity index 100% rename from src/Avalonia.Styling/Styling/SelectorMatch.cs rename to src/Avalonia.Base/Styling/SelectorMatch.cs diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Base/Styling/Selectors.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Selectors.cs rename to src/Avalonia.Base/Styling/Selectors.cs diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs new file mode 100644 index 0000000000..b4b3399022 --- /dev/null +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -0,0 +1,81 @@ +using System; +using Avalonia.Animation; +using Avalonia.Data; +using Avalonia.Data.Core; +using Avalonia.Metadata; +using Avalonia.Utilities; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// A setter for a . + /// + /// + /// A is used to set a value on a + /// depending on a condition. + /// + public class Setter : ISetter, IAnimationSetter + { + private object? _value; + + /// + /// Initializes a new instance of the class. + /// + public Setter() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The property to set. + /// The property value. + public Setter(AvaloniaProperty property, object value) + { + Property = property; + Value = value; + } + + /// + /// Gets or sets the property to set. + /// + public AvaloniaProperty? Property { get; set; } + + /// + /// Gets or sets the property value. + /// + [Content] + [AssignBinding] + [DependsOn(nameof(Property))] + public object? Value + { + get => _value; + set + { + (value as ISetterValue)?.Initialize(this); + _value = value; + } + } + + public ISetterInstance Instance(IStyleable target) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + + if (Property is null) + { + throw new InvalidOperationException("Setter.Property must be set."); + } + + return Property.CreateSetterInstance(target, Value); + } + + private struct SetterVisitorData + { + public IStyleable target; + public object? value; + public ISetterInstance? result; + } + } +} diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Style.cs rename to src/Avalonia.Base/Styling/Style.cs diff --git a/src/Avalonia.Styling/Styling/StyleInstance.cs b/src/Avalonia.Base/Styling/StyleInstance.cs similarity index 100% rename from src/Avalonia.Styling/Styling/StyleInstance.cs rename to src/Avalonia.Base/Styling/StyleInstance.cs diff --git a/src/Avalonia.Styling/Styling/Styler.cs b/src/Avalonia.Base/Styling/Styler.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Styler.cs rename to src/Avalonia.Base/Styling/Styler.cs diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs similarity index 100% rename from src/Avalonia.Styling/Styling/Styles.cs rename to src/Avalonia.Base/Styling/Styles.cs diff --git a/src/Avalonia.Styling/Styling/TemplateSelector.cs b/src/Avalonia.Base/Styling/TemplateSelector.cs similarity index 100% rename from src/Avalonia.Styling/Styling/TemplateSelector.cs rename to src/Avalonia.Base/Styling/TemplateSelector.cs diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs similarity index 100% rename from src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs rename to src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Base/Thickness.cs similarity index 100% rename from src/Avalonia.Visuals/Thickness.cs rename to src/Avalonia.Base/Thickness.cs diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs index 397826df53..6423d86e7c 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs @@ -46,7 +46,7 @@ namespace Avalonia.Threading { composite.Add(action(this, state)); } - }, DispatcherPriority.DataBind); + }, DispatcherPriority.Background); composite.Add(cancellation); diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 49cee441d0..2eb2e7c01f 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -83,42 +83,42 @@ namespace Avalonia.Threading _jobRunner.HasJobsWithPriority(minimumPriority); /// - public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Action action, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); return _jobRunner.InvokeAsync(action, priority); } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority); } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority).Unwrap(); } /// - public Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func> function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority).Unwrap(); } /// - public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); _jobRunner.Post(action, priority); } /// - public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, T arg, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); _jobRunner.Post(action, arg, priority); diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index a2b4b86bac..a93e4f406d 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -1,74 +1,121 @@ +using System; + namespace Avalonia.Threading { /// /// Defines the priorities with which jobs can be invoked on a . /// - // TODO: These are copied from WPF - many won't apply to Avalonia. - public enum DispatcherPriority + public readonly struct DispatcherPriority : IEquatable, IComparable { + /// + /// The integer value of the priority + /// + public int Value { get; } + + private DispatcherPriority(int value) + { + Value = value; + } + /// /// Minimum possible priority /// - MinValue = 1, - + public static readonly DispatcherPriority MinValue = new(0); + /// /// The job will be processed when the system is idle. /// - SystemIdle = 1, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinValue; /// /// The job will be processed when the application is idle. /// - ApplicationIdle = 2, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = MinValue; /// /// The job will be processed after background operations have completed. /// - ContextIdle = 3, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = MinValue; /// - /// The job will be processed after other non-idle operations have completed. + /// The job will be processed with normal priority. /// - Background = 4, + public static readonly DispatcherPriority Normal = MinValue; /// - /// The job will be processed with the same priority as input. + /// The job will be processed after other non-idle operations have completed. /// - Input = 5, + public static readonly DispatcherPriority Background = new(1); /// - /// The job will be processed after layout and render but before input. + /// The job will be processed with the same priority as input. /// - Loaded = 6, + public static readonly DispatcherPriority Input = new(2); /// - /// The job will be processed with the same priority as render. + /// The job will be processed after layout and render but before input. /// - Render = 7, + public static readonly DispatcherPriority Loaded = new(3); /// /// The job will be processed with the same priority as render. /// - Layout = 8, - + public static readonly DispatcherPriority Render = new(5); + /// - /// The job will be processed with the same priority as data binding. + /// The job will be processed with the same priority as render. /// - DataBind = 9, + public static readonly DispatcherPriority Layout = new(6); /// - /// The job will be processed with normal priority. + /// The job will be processed with the same priority as data binding. /// - Normal = 10, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = MinValue; /// /// The job will be processed before other asynchronous operations. /// - Send = 11, - + public static readonly DispatcherPriority Send = new(7); + /// /// Maximum possible priority /// - MaxValue = 11 + public static readonly DispatcherPriority MaxValue = Send; + + // Note: unlike ctor this one is validating + public static DispatcherPriority FromValue(int value) + { + if (value < MinValue.Value || value > MaxValue.Value) + throw new ArgumentOutOfRangeException(nameof(value)); + return new DispatcherPriority(value); + } + + public static implicit operator int(DispatcherPriority priority) => priority.Value; + + public static implicit operator DispatcherPriority(int value) => FromValue(value); + + /// + public bool Equals(DispatcherPriority other) => Value == other.Value; + + /// + public override bool Equals(object? obj) => obj is DispatcherPriority other && Equals(other); + + /// + public override int GetHashCode() => Value.GetHashCode(); + + public static bool operator ==(DispatcherPriority left, DispatcherPriority right) => left.Value == right.Value; + + public static bool operator !=(DispatcherPriority left, DispatcherPriority right) => left.Value != right.Value; + + public static bool operator <(DispatcherPriority left, DispatcherPriority right) => left.Value < right.Value; + + public static bool operator >(DispatcherPriority left, DispatcherPriority right) => left.Value > right.Value; + + public static bool operator <=(DispatcherPriority left, DispatcherPriority right) => left.Value <= right.Value; + + public static bool operator >=(DispatcherPriority left, DispatcherPriority right) => left.Value >= right.Value; + + /// + public int CompareTo(DispatcherPriority other) => Value.CompareTo(other.Value); } -} +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 93023b90a5..0c25d89722 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -123,7 +123,7 @@ namespace Avalonia.Threading /// The interval at which to tick. /// The priority to use. /// An used to cancel the timer. - public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = DispatcherPriority.Normal) + public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = default) { var timer = new DispatcherTimer(priority) { Interval = interval }; @@ -152,7 +152,7 @@ namespace Avalonia.Threading public static IDisposable RunOnce( Action action, TimeSpan interval, - DispatcherPriority priority = DispatcherPriority.Normal) + DispatcherPriority priority = default) { interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); diff --git a/src/Avalonia.Base/Threading/IDispatcher.cs b/src/Avalonia.Base/Threading/IDispatcher.cs index cd5add70d4..713a7ac4d7 100644 --- a/src/Avalonia.Base/Threading/IDispatcher.cs +++ b/src/Avalonia.Base/Threading/IDispatcher.cs @@ -24,16 +24,7 @@ namespace Avalonia.Threading /// /// The method. /// The priority with which to invoke the method. - void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal); - - /// - /// Posts an action that will be invoked on the dispatcher thread. - /// - /// type of argument - /// The method to call. - /// The argument of method to call. - /// The priority with which to invoke the method. - void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal); + void Post(Action action, DispatcherPriority priority = default); /// /// Invokes a action on the dispatcher thread. @@ -41,24 +32,7 @@ namespace Avalonia.Threading /// The method. /// The priority with which to invoke the method. /// A task that can be used to track the method's execution. - Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal); - - /// - /// Invokes a method on the dispatcher thread. - /// - /// The method. - /// The priority with which to invoke the method. - /// A task that can be used to track the method's execution. - Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal); - - /// - /// Queues the specified work to run on the dispatcher thread and returns a proxy for the - /// task returned by . - /// - /// The work to execute asynchronously. - /// The priority with which to invoke the method. - /// A task that represents a proxy for the task returned by . - Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal); + Task InvokeAsync(Action action, DispatcherPriority priority = default); /// /// Queues the specified work to run on the dispatcher thread and returns a proxy for the @@ -67,6 +41,6 @@ namespace Avalonia.Threading /// The work to execute asynchronously. /// The priority with which to invoke the method. /// A task that represents a proxy for the task returned by . - Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal); + Task InvokeAsync(Func function, DispatcherPriority priority = default); } } diff --git a/src/Avalonia.Visuals/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs similarity index 100% rename from src/Avalonia.Visuals/Utilities/ArrayBuilder.cs rename to src/Avalonia.Base/Utilities/ArrayBuilder.cs diff --git a/src/Avalonia.Visuals/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs similarity index 100% rename from src/Avalonia.Visuals/Utilities/ArraySlice.cs rename to src/Avalonia.Base/Utilities/ArraySlice.cs diff --git a/src/Avalonia.Visuals/Utilities/BinarySearchExtension.cs b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs similarity index 100% rename from src/Avalonia.Visuals/Utilities/BinarySearchExtension.cs rename to src/Avalonia.Base/Utilities/BinarySearchExtension.cs diff --git a/src/Avalonia.Base/Utilities/EnumHelper.cs b/src/Avalonia.Base/Utilities/EnumHelper.cs new file mode 100644 index 0000000000..c857033ef1 --- /dev/null +++ b/src/Avalonia.Base/Utilities/EnumHelper.cs @@ -0,0 +1,19 @@ +using System; + +namespace Avalonia.Utilities +{ + internal class EnumHelper + { +#if NET6_0_OR_GREATER + public static T Parse(ReadOnlySpan key, bool ignoreCase) where T : struct + { + return Enum.Parse(key, ignoreCase); + } +#else + public static T Parse(string key, bool ignoreCase) where T : struct + { + return (T)Enum.Parse(typeof(T), key, ignoreCase); + } +#endif + } +} diff --git a/src/Avalonia.Visuals/Utilities/FrugalList.cs b/src/Avalonia.Base/Utilities/FrugalList.cs similarity index 100% rename from src/Avalonia.Visuals/Utilities/FrugalList.cs rename to src/Avalonia.Base/Utilities/FrugalList.cs diff --git a/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs b/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs deleted file mode 100644 index 6a8df91b81..0000000000 --- a/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Avalonia.Utilities -{ - /// - /// A visitor to resolve an untyped to a typed property. - /// - /// The type of user data passed. - /// - /// Pass an instance that implements this interface to - /// - /// in order to resolve un untyped to a typed - /// or . - /// - public interface IAvaloniaPropertyVisitor - where TData : struct - { - /// - /// Called when the property is a styled property. - /// - /// The property value type. - /// The property. - /// The user data. - void Visit(StyledPropertyBase property, ref TData data); - - /// - /// Called when the property is a direct property. - /// - /// The property value type. - /// The property. - /// The user data. - void Visit(DirectPropertyBase property, ref TData data); - } -} diff --git a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs index e48c0cb111..6cf8c605a7 100644 --- a/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs +++ b/src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs @@ -9,4 +9,31 @@ namespace Avalonia.Utilities; public interface IWeakEventSubscriber where TEventArgs : EventArgs { void OnEvent(object? sender, WeakEvent ev, TEventArgs e); -} \ No newline at end of file +} + +public sealed class WeakEventSubscriber : IWeakEventSubscriber where TEventArgs : EventArgs +{ + public event Action? Event; + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, TEventArgs e) + { + Event?.Invoke(sender, ev, e); + } +} + +public sealed class TargetWeakEventSubscriber : IWeakEventSubscriber where TEventArgs : EventArgs +{ + private readonly TTarget _target; + private readonly Action _dispatchFunc; + + public TargetWeakEventSubscriber(TTarget target, Action dispatchFunc) + { + _target = target; + _dispatchFunc = dispatchFunc; + } + + void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, TEventArgs e) + { + _dispatchFunc(_target, sender, ev, e); + } +} diff --git a/src/Avalonia.Visuals/Utilities/MappedArraySlice.cs b/src/Avalonia.Base/Utilities/MappedArraySlice.cs similarity index 100% rename from src/Avalonia.Visuals/Utilities/MappedArraySlice.cs rename to src/Avalonia.Base/Utilities/MappedArraySlice.cs diff --git a/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs b/src/Avalonia.Base/Utilities/ReadOnlySlice.cs similarity index 100% rename from src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs rename to src/Avalonia.Base/Utilities/ReadOnlySlice.cs diff --git a/src/Avalonia.Visuals/Utilities/Span.cs b/src/Avalonia.Base/Utilities/Span.cs similarity index 100% rename from src/Avalonia.Visuals/Utilities/Span.cs rename to src/Avalonia.Base/Utilities/Span.cs diff --git a/src/Avalonia.Visuals/Utilities/ValueSpan.cs b/src/Avalonia.Base/Utilities/ValueSpan.cs similarity index 100% rename from src/Avalonia.Visuals/Utilities/ValueSpan.cs rename to src/Avalonia.Base/Utilities/ValueSpan.cs diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index 0b32015a8a..e72606bf70 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -36,7 +37,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { if (!_subscriptions.TryGetValue(target, out var subscription)) _subscriptions.Add(target, subscription = new Subscription(this, target)); - subscription.Add(new WeakReference>(subscriber)); + subscription.Add(subscriber); } public void Unsubscribe(TSender target, IWeakEventSubscriber subscriber) @@ -51,11 +52,59 @@ public class WeakEvent : WeakEvent where TEventArgs : Event private readonly TSender _target; private readonly Action _compact; - private WeakReference>?[] _data = - new WeakReference>[16]; - private int _count; + struct Entry + { + WeakReference>? _reference; + int _hashCode; + + public Entry(IWeakEventSubscriber r) + { + if (r == null) + { + _reference = null; + _hashCode = 0; + return; + } + + _hashCode = r.GetHashCode(); + _reference = new WeakReference>(r); + } + + public bool IsEmpty + { + get + { + if (_reference == null) + return true; + if (_reference.TryGetTarget(out _)) + return false; + _reference = null; + return true; + } + } + + public bool TryGetTarget([MaybeNullWhen(false)]out IWeakEventSubscriber target) + { + if (_reference == null) + { + target = null!; + return false; + } + return _reference.TryGetTarget(out target); + } + + public bool Equals(IWeakEventSubscriber r) + { + if (_reference == null || r.GetHashCode() != _hashCode) + return false; + return _reference.TryGetTarget(out var target) && target == r; + } + } + private readonly Action _unsubscribe; + private readonly WeakHashList> _list = new(); private bool _compactScheduled; + private bool _destroyed; public Subscription(WeakEvent ev, TSender target) { @@ -67,48 +116,27 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Destroy() { + if(_destroyed) + return; + _destroyed = true; _unsubscribe(); _ev._subscriptions.Remove(_target); } - public void Add(WeakReference> s) - { - if (_count == _data.Length) - { - //Extend capacity - var extendedData = new WeakReference>?[_data.Length * 2]; - Array.Copy(_data, extendedData, _data.Length); - _data = extendedData; - } - - _data[_count] = s; - _count++; - } + public void Add(IWeakEventSubscriber s) => _list.Add(s); public void Remove(IWeakEventSubscriber s) { - var removed = false; - - for (int c = 0; c < _count; ++c) - { - var reference = _data[c]; - - if (reference != null && reference.TryGetTarget(out var instance) && instance == s) - { - _data[c] = null; - removed = true; - } - } - - if (removed) - { + _list.Remove(s); + if(_list.IsEmpty) + Destroy(); + else if(_list.NeedCompact && _compactScheduled) ScheduleCompact(); - } } void ScheduleCompact() { - if(_compactScheduled) + if(_compactScheduled || _destroyed) return; _compactScheduled = true; Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); @@ -116,43 +144,27 @@ public class WeakEvent : WeakEvent where TEventArgs : Event void Compact() { + if(!_compactScheduled) + return; _compactScheduled = false; - int empty = -1; - for (var c = 0; c < _count; c++) - { - var r = _data[c]; - //Mark current index as first empty - if (r == null && empty == -1) - empty = c; - //If current element isn't null and we have an empty one - if (r != null && empty != -1) - { - _data[c] = null; - _data[empty] = r; - empty++; - } - } - - if (empty != -1) - _count = empty; - if (_count == 0) + _list.Compact(); + if (_list.IsEmpty) Destroy(); } void OnEvent(object? sender, TEventArgs eventArgs) { - var needCompact = false; - for (var c = 0; c < _count; c++) + var alive = _list.GetAlive(); + if(alive == null) + Destroy(); + else { - var r = _data[c]; - if (r?.TryGetTarget(out var sub) == true) - sub!.OnEvent(_target, _ev, eventArgs); - else - needCompact = true; + foreach(var item in alive.Span) + item.OnEvent(_target, _ev, eventArgs); + WeakHashList>.ReturnToSharedPool(alive); + if(_list.NeedCompact && !_compactScheduled) + ScheduleCompact(); } - - if (needCompact) - ScheduleCompact(); } } diff --git a/src/Avalonia.Base/Utilities/WeakHashList.cs b/src/Avalonia.Base/Utilities/WeakHashList.cs new file mode 100644 index 0000000000..df480aa062 --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakHashList.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Utilities; + +internal class WeakHashList where T : class +{ + public const int DefaultArraySize = 8; + + private struct Key + { + public WeakReference? Weak; + public T? Strong; + public int HashCode; + + public static Key MakeStrong(T r) => new() + { + HashCode = r.GetHashCode(), + Strong = r + }; + + public static Key MakeWeak(T r) => new() + { + HashCode = r.GetHashCode(), + Weak = new WeakReference(r) + }; + + public override int GetHashCode() => HashCode; + } + + class KeyComparer : IEqualityComparer + { + public bool Equals(Key x, Key y) + { + if (x.HashCode != y.HashCode) + return false; + if (x.Strong != null) + { + if (y.Strong != null) + return x.Strong == y.Strong; + if (y.Weak == null) + return false; + return y.Weak.TryGetTarget(out var weakTarget) && weakTarget == x.Strong; + } + else if (y.Strong != null) + { + if (x.Weak == null) + return false; + return x.Weak.TryGetTarget(out var weakTarget) && weakTarget == y.Strong; + } + else + { + if (x.Weak == null || x.Weak.TryGetTarget(out var xTarget) == false) + return y.Weak?.TryGetTarget(out _) != true; + return y.Weak?.TryGetTarget(out var yTarget) == true && xTarget == yTarget; + } + } + + public int GetHashCode(Key obj) => obj.HashCode; + public static KeyComparer Instance = new(); + } + + Dictionary? _dic; + WeakReference?[]? _arr; + int _arrCount; + + public bool IsEmpty => _dic is not null ? _dic.Count == 0 : _arrCount == 0; + + public bool NeedCompact { get; private set; } + + public void Add(T item) + { + if (_dic != null) + { + var strongKey = Key.MakeStrong(item); + if (_dic.TryGetValue(strongKey, out var cnt)) + _dic[strongKey] = cnt + 1; + else + _dic[Key.MakeWeak(item)] = 1; + return; + } + + if (_arr == null) + _arr = new WeakReference[DefaultArraySize]; + + if (_arrCount < _arr.Length) + { + _arr[_arrCount] = new WeakReference(item); + _arrCount++; + return; + } + + // Check if something is dead + for (var c = 0; c < _arrCount; c++) + { + if (_arr[c]!.TryGetTarget(out _) == false) + { + _arr[c] = new WeakReference(item); + return; + } + } + + _dic = new Dictionary(KeyComparer.Instance); + foreach (var existing in _arr) + { + if (existing!.TryGetTarget(out var target)) + Add(target); + } + + Add(item); + + _arr = null; + _arrCount = 0; + } + + public void Remove(T item) + { + if (_arr != null) + { + for (var c = 0; c < _arr.Length; c++) + { + if (_arr[c]?.TryGetTarget(out var target) == true && target == item) + { + _arr[c] = null; + ArrCompact(); + return; + } + } + } + else if (_dic != null) + { + var strongKey = Key.MakeStrong(item); + + if (_dic.TryGetValue(strongKey, out var cnt)) + { + if (cnt > 1) + { + _dic[strongKey] = cnt - 1; + return; + } + } + + _dic.Remove(strongKey); + } + } + + private void ArrCompact() + { + if (_arr != null) + { + int empty = -1; + for (var c = 0; c < _arrCount; c++) + { + var r = _arr[c]; + //Mark current index as first empty + if (r == null && empty == -1) + empty = c; + //If current element isn't null and we have an empty one + if (r != null && empty != -1) + { + _arr[c] = null; + _arr[empty] = r; + empty++; + } + } + + if (empty != -1) + _arrCount = empty; + } + } + + public void Compact() + { + if (_dic != null) + { + PooledList? toRemove = null; + foreach (var kvp in _dic) + { + if (kvp.Key.Weak?.TryGetTarget(out _) != true) + (toRemove ??= new PooledList()).Add(kvp.Key); + } + + if (toRemove != null) + { + foreach (var k in toRemove) + _dic.Remove(k); + toRemove.Dispose(); + } + } + } + + private static readonly Stack> s_listPool = new(); + + public static void ReturnToSharedPool(PooledList list) + { + list.Clear(); + s_listPool.Push(list); + } + + public PooledList? GetAlive(Func>? factory = null) + { + PooledList? pooled = null; + if (_arr != null) + { + bool needCompact = false; + for (var c = 0; c < _arrCount; c++) + { + if (_arr[c]?.TryGetTarget(out var target) == true) + (pooled ??= factory?.Invoke() + ?? (s_listPool.Count > 0 + ? s_listPool.Pop() + : new PooledList())).Add(target!); + else + { + _arr[c] = null; + needCompact = true; + } + } + if(needCompact) + ArrCompact(); + return pooled; + } + if (_dic != null) + { + foreach (var kvp in _dic) + { + if (kvp.Key.Weak?.TryGetTarget(out var target) == true) + (pooled ??= factory?.Invoke() + ?? (s_listPool.Count > 0 + ? s_listPool.Pop() + : new PooledList())) + .Add(target!); + else + NeedCompact = true; + } + } + + return pooled; + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 6b7b598086..69c644dff9 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -21,16 +21,15 @@ namespace Avalonia /// - For a single binding it will be an instance of /// - For all other cases it will be an instance of /// - internal class ValueStore : IValueSink + internal class ValueStore { private readonly AvaloniaObject _owner; - private readonly IValueSink _sink; private readonly AvaloniaPropertyValueStore _values; private BatchUpdate? _batchUpdate; public ValueStore(AvaloniaObject owner) { - _sink = _owner = owner; + _owner = owner; _values = new AvaloniaPropertyValueStore(); } @@ -122,7 +121,7 @@ namespace Avalonia } else { - var entry = new ConstantValueEntry(property, value, priority, this); + var entry = new ConstantValueEntry(property, value, priority, new(this)); AddValue(property, entry); NotifyValueChanged(property, default, value, priority); result = entry; @@ -151,7 +150,7 @@ namespace Avalonia } else { - var entry = new BindingEntry(_owner, property, source, priority, this); + var entry = new BindingEntry(_owner, property, source, priority, new(this)); AddValue(property, entry); return entry; } @@ -187,7 +186,7 @@ namespace Avalonia // so there's no way to mark them for removal at the end of a batch update. Instead convert // them to a constant value entry with Unset priority in the event of a local value being // cleared during a batch update. - var sentinel = new ConstantValueEntry(property, Optional.Empty, BindingPriority.Unset, _sink); + var sentinel = new ConstantValueEntry(property, Optional.Empty, BindingPriority.Unset, new(this)); _values.SetValue(property, sentinel); } @@ -196,11 +195,11 @@ namespace Avalonia } } - public void CoerceValue(StyledPropertyBase property) + public void CoerceValue(AvaloniaProperty property) { if (TryGetValue(property, out var slot)) { - if (slot is PriorityValue p) + if (slot is IPriorityValue p) { p.UpdateEffectiveValue(); } @@ -222,7 +221,7 @@ namespace Avalonia return null; } - void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change) + public void ValueChanged(AvaloniaPropertyChangedEventArgs change) { if (_batchUpdate is object) { @@ -233,11 +232,11 @@ namespace Avalonia } else { - _sink.ValueChanged(change); + _owner.ValueChanged(change); } } - void IValueSink.Completed( + public void Completed( StyledPropertyBase property, IPriorityValueEntry entry, Optional oldValue) @@ -248,7 +247,7 @@ namespace Avalonia if (_batchUpdate is null) { _values.Remove(property); - _sink.Completed(property, entry, oldValue); + _owner.Completed(property, entry, oldValue); } else { @@ -352,7 +351,7 @@ namespace Avalonia { if (_batchUpdate is null) { - _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( + _owner.ValueChanged(new AvaloniaPropertyChangedEventArgs( _owner, property, oldValue, @@ -451,7 +450,7 @@ namespace Avalonia }; // Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs. - slot.RaiseValueChanged(_owner._sink, _owner._owner, entry.property, oldValue, newValue); + slot.RaiseValueChanged(_owner._owner, entry.property, oldValue, newValue); // During batch update values can't be removed immediately because they're needed to raise // the _sink.ValueChanged notification. They instead mark themselves for removal by setting diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Base/Vector.cs similarity index 100% rename from src/Avalonia.Visuals/Vector.cs rename to src/Avalonia.Base/Vector.cs diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs new file mode 100644 index 0000000000..4fd21f02f9 --- /dev/null +++ b/src/Avalonia.Base/Visual.cs @@ -0,0 +1,679 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using Avalonia.Collections; +using Avalonia.Data; +using Avalonia.Logging; +using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.Metadata; +using Avalonia.Rendering; +using Avalonia.Utilities; +using Avalonia.VisualTree; + +#nullable enable + +namespace Avalonia +{ + /// + /// Base class for controls that provides rendering and related visual properties. + /// + /// + /// The class represents elements that have a visual on-screen + /// representation and stores all the information needed for an + /// to render the control. To traverse the visual tree, use the + /// extension methods defined in . + /// + [UsableDuringInitialization] + public class Visual : StyledElement, IVisual + { + /// + /// Defines the property. + /// + public static readonly DirectProperty BoundsProperty = + AvaloniaProperty.RegisterDirect(nameof(Bounds), o => o.Bounds); + + public static readonly DirectProperty TransformedBoundsProperty = + AvaloniaProperty.RegisterDirect( + nameof(TransformedBounds), + o => o.TransformedBounds); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ClipToBoundsProperty = + AvaloniaProperty.Register(nameof(ClipToBounds)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ClipProperty = + AvaloniaProperty.Register(nameof(Clip)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsVisibleProperty = + AvaloniaProperty.Register(nameof(IsVisible), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OpacityProperty = + AvaloniaProperty.Register(nameof(Opacity), 1); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OpacityMaskProperty = + AvaloniaProperty.Register(nameof(OpacityMask)); + + /// + /// Defines the property. + /// + public static readonly DirectProperty HasMirrorTransformProperty = + AvaloniaProperty.RegisterDirect(nameof(HasMirrorTransform), o => o.HasMirrorTransform); + + /// + /// Defines the property. + /// + public static readonly StyledProperty RenderTransformProperty = + AvaloniaProperty.Register(nameof(RenderTransform)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty RenderTransformOriginProperty = + AvaloniaProperty.Register(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center); + + /// + /// Defines the property. + /// + public static readonly DirectProperty VisualParentProperty = + AvaloniaProperty.RegisterDirect(nameof(IVisual.VisualParent), o => o._visualParent); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ZIndexProperty = + AvaloniaProperty.Register(nameof(ZIndex)); + + private static readonly WeakEvent InvalidatedWeakEvent = + WeakEvent.Register( + (s, h) => s.Invalidated += h, + (s, h) => s.Invalidated -= h); + + private Rect _bounds; + private TransformedBounds? _transformedBounds; + private IRenderRoot? _visualRoot; + private IVisual? _visualParent; + private bool _hasMirrorTransform; + private TargetWeakEventSubscriber? _affectsRenderWeakSubscriber; + + /// + /// Initializes static members of the class. + /// + static Visual() + { + AffectsRender( + BoundsProperty, + ClipProperty, + ClipToBoundsProperty, + IsVisibleProperty, + OpacityProperty, + HasMirrorTransformProperty); + RenderTransformProperty.Changed.Subscribe(RenderTransformChanged); + ZIndexProperty.Changed.Subscribe(ZIndexChanged); + } + + /// + /// Initializes a new instance of the class. + /// + public Visual() + { + // Disable transitions until we're added to the visual tree. + DisableTransitions(); + + var visualChildren = new AvaloniaList(); + visualChildren.ResetBehavior = ResetBehavior.Remove; + visualChildren.Validate = visual => ValidateVisualChild(visual); + visualChildren.CollectionChanged += VisualChildrenChanged; + VisualChildren = visualChildren; + } + + /// + /// Raised when the control is attached to a rooted visual tree. + /// + public event EventHandler? AttachedToVisualTree; + + /// + /// Raised when the control is detached from a rooted visual tree. + /// + public event EventHandler? DetachedFromVisualTree; + + /// + /// Gets the bounds of the control relative to its parent. + /// + public Rect Bounds + { + get { return _bounds; } + protected set { SetAndRaise(BoundsProperty, ref _bounds, value); } + } + + /// + /// Gets the bounds of the control relative to the window, accounting for rendering transforms. + /// + public TransformedBounds? TransformedBounds => _transformedBounds; + + /// + /// Gets or sets a value indicating whether the control should be clipped to its bounds. + /// + public bool ClipToBounds + { + get { return GetValue(ClipToBoundsProperty); } + set { SetValue(ClipToBoundsProperty, value); } + } + + /// + /// Gets or sets the geometry clip for this visual. + /// + public Geometry? Clip + { + get { return GetValue(ClipProperty); } + set { SetValue(ClipProperty, value); } + } + + /// + /// Gets a value indicating whether this control and all its parents are visible. + /// + public bool IsEffectivelyVisible + { + get + { + IVisual? node = this; + + while (node != null) + { + if (!node.IsVisible) + { + return false; + } + + node = node.VisualParent; + } + + return true; + } + } + + /// + /// Gets or sets a value indicating whether this control is visible. + /// + public bool IsVisible + { + get { return GetValue(IsVisibleProperty); } + set { SetValue(IsVisibleProperty, value); } + } + + /// + /// Gets or sets the opacity of the control. + /// + public double Opacity + { + get { return GetValue(OpacityProperty); } + set { SetValue(OpacityProperty, value); } + } + + /// + /// Gets or sets the opacity mask of the control. + /// + public IBrush? OpacityMask + { + get { return GetValue(OpacityMaskProperty); } + set { SetValue(OpacityMaskProperty, value); } + } + + /// + /// Gets or sets a value indicating whether to apply mirror transform on this control. + /// + public bool HasMirrorTransform + { + get { return _hasMirrorTransform; } + protected set { SetAndRaise(HasMirrorTransformProperty, ref _hasMirrorTransform, value); } + } + + /// + /// Gets or sets the render transform of the control. + /// + public ITransform? RenderTransform + { + get { return GetValue(RenderTransformProperty); } + set { SetValue(RenderTransformProperty, value); } + } + + /// + /// Gets or sets the transform origin of the control. + /// + public RelativePoint RenderTransformOrigin + { + get { return GetValue(RenderTransformOriginProperty); } + set { SetValue(RenderTransformOriginProperty, value); } + } + + /// + /// Gets or sets the Z index of the control. + /// + /// + /// Controls with a higher will appear in front of controls with + /// a lower ZIndex. If two controls have the same ZIndex then the control that appears + /// later in the containing element's children collection will appear on top. + /// + public int ZIndex + { + get { return GetValue(ZIndexProperty); } + set { SetValue(ZIndexProperty, value); } + } + + /// + /// Gets the control's child visuals. + /// + protected IAvaloniaList VisualChildren + { + get; + private set; + } + + /// + /// Gets the root of the visual tree, if the control is attached to a visual tree. + /// + protected IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot); + + /// + /// Gets a value indicating whether this control is attached to a visual root. + /// + bool IVisual.IsAttachedToVisualTree => VisualRoot != null; + + /// + /// Gets the control's child controls. + /// + IAvaloniaReadOnlyList IVisual.VisualChildren => VisualChildren; + + /// + /// Gets the control's parent visual. + /// + IVisual? IVisual.VisualParent => _visualParent; + + /// + /// Gets the root of the visual tree, if the control is attached to a visual tree. + /// + IRenderRoot? IVisual.VisualRoot => VisualRoot; + + TransformedBounds? IVisual.TransformedBounds + { + get { return _transformedBounds; } + set { SetAndRaise(TransformedBoundsProperty, ref _transformedBounds, value); } + } + + /// + /// Invalidates the visual and queues a repaint. + /// + public void InvalidateVisual() + { + VisualRoot?.Renderer?.AddDirty(this); + } + + /// + /// Renders the visual to a . + /// + /// The drawing context. + public virtual void Render(DrawingContext context) + { + Contract.Requires(context != null); + } + + /// + /// Indicates that a property change should cause to be + /// called. + /// + /// The properties. + /// + /// This method should be called in a control's static constructor with each property + /// on the control which when changed should cause a redraw. This is similar to WPF's + /// FrameworkPropertyMetadata.AffectsRender flag. + /// + [Obsolete("Use AffectsRender and specify the control type.")] + protected static void AffectsRender(params AvaloniaProperty[] properties) + { + AffectsRender(properties); + } + + /// + /// Indicates that a property change should cause to be + /// called. + /// + /// The control which the property affects. + /// The properties. + /// + /// This method should be called in a control's static constructor with each property + /// on the control which when changed should cause a redraw. This is similar to WPF's + /// FrameworkPropertyMetadata.AffectsRender flag. + /// + protected static void AffectsRender(params AvaloniaProperty[] properties) + where T : Visual + { + static void Invalidate(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is T sender) + { + sender.InvalidateVisual(); + } + } + + static void InvalidateAndSubscribe(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is T sender) + { + if (e.OldValue is IAffectsRender oldValue) + { + if (sender._affectsRenderWeakSubscriber != null) + InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber); + } + + if (e.NewValue is IAffectsRender newValue) + { + if (sender._affectsRenderWeakSubscriber == null) + { + sender._affectsRenderWeakSubscriber = new TargetWeakEventSubscriber( + sender, static (target, _, _, _) => + { + target.InvalidateVisual(); + }); + } + InvalidatedWeakEvent.Subscribe(newValue, sender._affectsRenderWeakSubscriber); + } + + sender.InvalidateVisual(); + } + } + + foreach (var property in properties) + { + if (property.CanValueAffectRender()) + { + property.Changed.Subscribe(e => InvalidateAndSubscribe(e)); + } + else + { + property.Changed.Subscribe(e => Invalidate(e)); + } + } + } + + protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + base.LogicalChildrenCollectionChanged(sender, e); + VisualRoot?.Renderer?.RecalculateChildren(this); + } + + /// + /// Calls the method + /// for this control and all of its visual descendants. + /// + /// The event args. + protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Attached to visual tree"); + + _visualRoot = e.Root; + + if (RenderTransform is IMutableTransform mutableTransform) + { + mutableTransform.Changed += RenderTransformChanged; + } + + EnableTransitions(); + OnAttachedToVisualTree(e); + AttachedToVisualTree?.Invoke(this, e); + InvalidateVisual(); + + var visualChildren = VisualChildren; + + if (visualChildren != null) + { + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) + { + if (visualChildren[i] is Visual child) + { + child.OnAttachedToVisualTreeCore(e); + } + } + } + } + + /// + /// Calls the method + /// for this control and all of its visual descendants. + /// + /// The event args. + protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree"); + + _visualRoot = null; + + if (RenderTransform is IMutableTransform mutableTransform) + { + mutableTransform.Changed -= RenderTransformChanged; + } + + DisableTransitions(); + OnDetachedFromVisualTree(e); + DetachedFromVisualTree?.Invoke(this, e); + e.Root?.Renderer?.AddDirty(this); + + var visualChildren = VisualChildren; + + if (visualChildren != null) + { + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) + { + if (visualChildren[i] is Visual child) + { + child.OnDetachedFromVisualTreeCore(e); + } + } + } + } + + /// + /// Called when the control is added to a rooted visual tree. + /// + /// The event args. + protected virtual void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + } + + /// + /// Called when the control is removed from a rooted visual tree. + /// + /// The event args. + protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + } + + /// + /// Called when the control's visual parent changes. + /// + /// The old visual parent. + /// The new visual parent. + protected virtual void OnVisualParentChanged(IVisual? oldParent, IVisual? newParent) + { + RaisePropertyChanged( + VisualParentProperty, + new Optional(oldParent), + new BindingValue(newParent), + BindingPriority.LocalValue); + } + + protected internal sealed override void LogBindingError(AvaloniaProperty property, Exception e) + { + // Don't log a binding error unless the control is attached to a logical tree. + if (((ILogical)this).IsAttachedToLogicalTree) + { + if (e is BindingChainException b && + string.IsNullOrEmpty(b.ExpressionErrorPoint) && + DataContext == null) + { + // The error occurred at the root of the binding chain and DataContext is null; + // don't log this - the DataContext probably hasn't been set up yet. + return; + } + + Logger.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log( + this, + "Error in binding to {Target}.{Property}: {Message}", + this, + property, + e.Message); + } + } + + /// + /// Called when a visual's changes. + /// + /// The event args. + private static void RenderTransformChanged(AvaloniaPropertyChangedEventArgs e) + { + var sender = e.Sender as Visual; + + if (sender?.VisualRoot != null) + { + var oldValue = e.OldValue as Transform; + var newValue = e.NewValue as Transform; + + if (oldValue != null) + { + oldValue.Changed -= sender.RenderTransformChanged; + } + + if (newValue != null) + { + newValue.Changed += sender.RenderTransformChanged; + } + + sender.InvalidateVisual(); + } + } + + /// + /// Ensures a visual child is not null and not already parented. + /// + /// The visual child. + private static void ValidateVisualChild(IVisual c) + { + if (c == null) + { + throw new ArgumentNullException(nameof(c), "Cannot add null to VisualChildren."); + } + + if (c.VisualParent != null) + { + throw new InvalidOperationException("The control already has a visual parent."); + } + } + + /// + /// Called when the property changes on any control. + /// + /// The event args. + private static void ZIndexChanged(AvaloniaPropertyChangedEventArgs e) + { + var sender = e.Sender as IVisual; + var parent = sender?.VisualParent; + sender?.InvalidateVisual(); + parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); + } + + /// + /// Called when the 's event + /// is fired. + /// + /// The sender. + /// The event args. + private void RenderTransformChanged(object? sender, EventArgs e) + { + InvalidateVisual(); + } + + /// + /// Sets the visual parent of the Visual. + /// + /// The visual parent. + private void SetVisualParent(Visual? value) + { + if (_visualParent == value) + { + return; + } + + var old = _visualParent; + _visualParent = value; + + if (_visualRoot != null) + { + var e = new VisualTreeAttachmentEventArgs(old!, _visualRoot); + OnDetachedFromVisualTreeCore(e); + } + + if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) + { + var root = this.FindAncestorOfType() ?? + throw new AvaloniaInternalException("Visual is atached to visual tree but root could not be found."); + var e = new VisualTreeAttachmentEventArgs(_visualParent, root); + OnAttachedToVisualTreeCore(e); + } + + OnVisualParentChanged(old, value); + } + + /// + /// Called when the collection changes. + /// + /// The sender. + /// The event args. + private void VisualChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + SetVisualParent(e.NewItems!, this); + break; + + case NotifyCollectionChangedAction.Remove: + SetVisualParent(e.OldItems!, null); + break; + + case NotifyCollectionChangedAction.Replace: + SetVisualParent(e.OldItems!, null); + SetVisualParent(e.NewItems!, this); + break; + } + } + + private static void SetVisualParent(IList children, Visual? parent) + { + var count = children.Count; + + for (var i = 0; i < count; i++) + { + var visual = (Visual) children[i]!; + + visual.SetVisualParent(parent); + } + } + } +} diff --git a/src/Avalonia.Base/VisualExtensions.cs b/src/Avalonia.Base/VisualExtensions.cs new file mode 100644 index 0000000000..3a3c2693d0 --- /dev/null +++ b/src/Avalonia.Base/VisualExtensions.cs @@ -0,0 +1,137 @@ +using System; +using Avalonia.VisualTree; + +namespace Avalonia +{ + /// + /// Extension methods for . + /// + public static class VisualExtensions + { + /// + /// Converts a point from screen to client coordinates. + /// + /// The visual. + /// The point in screen coordinates. + /// The point in client coordinates. + public static Point PointToClient(this IVisual visual, PixelPoint point) + { + var root = visual.VisualRoot ?? + throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); + var rootPoint = root.PointToClient(point); + return root.TranslatePoint(rootPoint, visual)!.Value; + } + + /// + /// Converts a point from client to screen coordinates. + /// + /// The visual. + /// The point in client coordinates. + /// The point in screen coordinates. + public static PixelPoint PointToScreen(this IVisual visual, Point point) + { + var root = visual.VisualRoot ?? + throw new ArgumentException("Control does not belong to a visual tree.", nameof(visual)); + var p = visual.TranslatePoint(point, root); + return visual.VisualRoot.PointToScreen(p!.Value); + } + + /// + /// Returns a transform that transforms the visual's coordinates into the coordinates + /// of the specified . + /// + /// The visual whose coordinates are to be transformed. + /// The visual to translate the coordinates to. + /// + /// A containing the transform or null if the visuals don't share a + /// common ancestor. + /// + public static Matrix? TransformToVisual(this IVisual from, IVisual to) + { + var common = from.FindCommonVisualAncestor(to); + + if (common != null) + { + var thisOffset = GetOffsetFrom(common, from); + var thatOffset = GetOffsetFrom(common, to); + + if (!thatOffset.TryInvert(out var thatOffsetInverted)) + { + return null; + } + + return thatOffsetInverted * thisOffset; + } + + return null; + } + + /// + /// Translates a point relative to this visual to coordinates that are relative to the specified visual. + /// + /// The visual. + /// The point value, as relative to this visual. + /// The visual to translate the given point into. + /// + /// A point value, now relative to the target visual rather than this source element, or null if the + /// two elements have no common ancestor. + /// + public static Point? TranslatePoint(this IVisual visual, Point point, IVisual relativeTo) + { + var transform = visual.TransformToVisual(relativeTo); + + if (transform.HasValue) + { + return point.Transform(transform.Value); + } + + return null; + } + + /// + /// Gets a transform from an ancestor to a descendent. + /// + /// The ancestor visual. + /// The visual. + /// The transform. + private static Matrix GetOffsetFrom(IVisual ancestor, IVisual visual) + { + var result = Matrix.Identity; + IVisual? v = visual; + + while (v != ancestor) + { + if (v.RenderTransform?.Value != null) + { + var origin = v.RenderTransformOrigin.ToPixels(v.Bounds.Size); + var offset = Matrix.CreateTranslation(origin); + var renderTransform = (-offset) * v.RenderTransform.Value * (offset); + + result *= renderTransform; + } + + if (v.HasMirrorTransform) + { + var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, v.Bounds.Width, 0); + result *= mirrorMatrix; + } + + var topLeft = v.Bounds.TopLeft; + + if (topLeft != default) + { + result *= Matrix.CreateTranslation(topLeft); + } + + v = v.VisualParent; + + if (v == null) + { + throw new ArgumentException("'visual' is not a descendant of 'ancestor'."); + } + } + + return result; + } + } +} diff --git a/src/Avalonia.Visuals/VisualTree/IHostedVisualTreeRoot.cs b/src/Avalonia.Base/VisualTree/IHostedVisualTreeRoot.cs similarity index 100% rename from src/Avalonia.Visuals/VisualTree/IHostedVisualTreeRoot.cs rename to src/Avalonia.Base/VisualTree/IHostedVisualTreeRoot.cs diff --git a/src/Avalonia.Base/VisualTree/IVisual.cs b/src/Avalonia.Base/VisualTree/IVisual.cs new file mode 100644 index 0000000000..b1251618c4 --- /dev/null +++ b/src/Avalonia.Base/VisualTree/IVisual.cs @@ -0,0 +1,124 @@ +using System; +using Avalonia.Collections; +using Avalonia.Media; +using Avalonia.Rendering; + +#nullable enable + +namespace Avalonia.VisualTree +{ + /// + /// Represents control that has a visual on-screen representation. + /// + /// + /// The interface defines the interface required for a renderer to + /// render a control. You should not usually need to reference this interface unless + /// you are writing a renderer; instead use the extension methods defined in + /// to traverse the visual tree. This interface is + /// implemented by . It should not be necessary to implement it + /// anywhere else. + /// + public interface IVisual + { + /// + /// Raised when the control is attached to a rooted visual tree. + /// + event EventHandler? AttachedToVisualTree; + + /// + /// Raised when the control is detached from a rooted visual tree. + /// + event EventHandler? DetachedFromVisualTree; + + /// + /// Gets the bounds of the control relative to its parent. + /// + Rect Bounds { get; } + + /// + /// Gets the bounds of the control relative to the window, accounting for rendering transforms. + /// + TransformedBounds? TransformedBounds { get; set; } + + /// + /// Gets a value indicating whether the control should be clipped to its bounds. + /// + bool ClipToBounds { get; set; } + + /// + /// Gets or sets the geometry clip for this visual. + /// + Geometry? Clip { get; set; } + + /// + /// Gets a value indicating whether this control is attached to a visual root. + /// + bool IsAttachedToVisualTree { get; } + + /// + /// Gets a value indicating whether this control and all its parents are visible. + /// + bool IsEffectivelyVisible { get; } + + /// + /// Gets or sets a value indicating whether this control is visible. + /// + bool IsVisible { get; set; } + + /// + /// Gets or sets the opacity of the control. + /// + double Opacity { get; set; } + + /// + /// Gets or sets the opacity mask for the control. + /// + IBrush? OpacityMask { get; set; } + + /// + /// Gets a value indicating whether to apply mirror transform on this control. + /// + bool HasMirrorTransform { get; } + + /// + /// Gets or sets the render transform of the control. + /// + ITransform? RenderTransform { get; set; } + + /// + /// Gets or sets the render transform origin of the control. + /// + RelativePoint RenderTransformOrigin { get; set; } + + /// + /// Gets the control's child visuals. + /// + IAvaloniaReadOnlyList VisualChildren { get; } + + /// + /// Gets the control's parent visual. + /// + IVisual? VisualParent { get; } + + /// + /// Gets the root of the visual tree, if the control is attached to a visual tree. + /// + IRenderRoot? VisualRoot { get; } + + /// + /// Gets or sets the Z index of the node. + /// + int ZIndex { get; set; } + + /// + /// Invalidates the visual and queues a repaint. + /// + void InvalidateVisual(); + + /// + /// Renders the control to a . + /// + /// The context. + void Render(DrawingContext context); + } +} diff --git a/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs b/src/Avalonia.Base/VisualTree/IVisualTreeHost.cs similarity index 100% rename from src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs rename to src/Avalonia.Base/VisualTree/IVisualTreeHost.cs diff --git a/src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs b/src/Avalonia.Base/VisualTree/IVisualWithRoundRectClip.cs similarity index 100% rename from src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs rename to src/Avalonia.Base/VisualTree/IVisualWithRoundRectClip.cs diff --git a/src/Avalonia.Visuals/VisualTree/TransformedBounds.cs b/src/Avalonia.Base/VisualTree/TransformedBounds.cs similarity index 100% rename from src/Avalonia.Visuals/VisualTree/TransformedBounds.cs rename to src/Avalonia.Base/VisualTree/TransformedBounds.cs diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Base/VisualTree/VisualExtensions.cs similarity index 100% rename from src/Avalonia.Visuals/VisualTree/VisualExtensions.cs rename to src/Avalonia.Base/VisualTree/VisualExtensions.cs diff --git a/src/Avalonia.Visuals/VisualTree/VisualLocator.cs b/src/Avalonia.Base/VisualTree/VisualLocator.cs similarity index 100% rename from src/Avalonia.Visuals/VisualTree/VisualLocator.cs rename to src/Avalonia.Base/VisualTree/VisualLocator.cs diff --git a/src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs b/src/Avalonia.Base/VisualTreeAttachmentEventArgs.cs similarity index 100% rename from src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs rename to src/Avalonia.Base/VisualTreeAttachmentEventArgs.cs diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 5a7daa6d12..e9b99c9aa8 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -62,36 +62,42 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) - + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + diff --git a/src/Avalonia.Build.Tasks/Properties/launchSettings.json b/src/Avalonia.Build.Tasks/Properties/launchSettings.json new file mode 100644 index 0000000000..e9f5af46d6 --- /dev/null +++ b/src/Avalonia.Build.Tasks/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Compile Sandbox": { + "commandName": "Project", + "executablePath": "$(SolutionDir)\\src\\Avalonia.Build.Tasks\\bin\\Debug\\net6.0\\Avalonia.Build.Tasks.exe", + "commandLineArgs": "$(SolutionDir)\\samples\\Sandbox\\obj\\Debug\\net6.0\\Avalonia\\original.dll $(SolutionDir)\\samples\\Sandbox\\bin\\Debug\\net6.0\\Sandbox.dll.refs $(SolutionDir)\\out.dll" + } + } +} diff --git a/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt b/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt index fcc74cf864..bfcec2960a 100644 --- a/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt @@ -1 +1,4 @@ -Total Issues: 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/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj index c89157dc0f..410cac72fc 100644 --- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj +++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj @@ -4,14 +4,8 @@ Avalonia.Controls.DataGrid - - - - - - @@ -23,4 +17,5 @@ + diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 5d71a499e3..aaac3f8f9c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -32,6 +32,14 @@ namespace Avalonia.Controls /// /// Displays data in a customizable grid. /// + [TemplatePart(DATAGRID_elementBottomRightCornerHeaderName, typeof(IVisual))] + [TemplatePart(DATAGRID_elementColumnHeadersPresenterName, typeof(DataGridColumnHeadersPresenter))] + [TemplatePart(DATAGRID_elementFrozenColumnScrollBarSpacerName, typeof(Control))] + [TemplatePart(DATAGRID_elementHorizontalScrollbarName, typeof(ScrollBar))] + [TemplatePart(DATAGRID_elementRowsPresenterName, typeof(DataGridRowsPresenter))] + [TemplatePart(DATAGRID_elementTopLeftCornerHeaderName, typeof(ContentControl))] + [TemplatePart(DATAGRID_elementTopRightCornerHeaderName, typeof(ContentControl))] + [TemplatePart(DATAGRID_elementVerticalScrollbarName, typeof(ScrollBar))] [PseudoClasses(":invalid", ":empty-rows", ":empty-columns")] public partial class DataGrid : TemplatedControl { @@ -669,8 +677,6 @@ namespace Avalonia.Controls ItemsProperty.Changed.AddClassHandler((x, e) => x.OnItemsPropertyChanged(e)); CanUserResizeColumnsProperty.Changed.AddClassHandler((x, e) => x.OnCanUserResizeColumnsChanged(e)); ColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthChanged(e)); - RowBackgroundProperty.Changed.AddClassHandler((x, e) => x.OnRowBackgroundChanged(e)); - AlternatingRowBackgroundProperty.Changed.AddClassHandler((x, e) => x.OnRowBackgroundChanged(e)); FrozenColumnCountProperty.Changed.AddClassHandler((x, e) => x.OnFrozenColumnCountChanged(e)); GridLinesVisibilityProperty.Changed.AddClassHandler((x, e) => x.OnGridLinesVisibilityChanged(e)); HeadersVisibilityProperty.Changed.AddClassHandler((x, e) => x.OnHeadersVisibilityChanged(e)); @@ -1144,14 +1150,6 @@ namespace Avalonia.Controls InvalidateCellsArrange(); } - private void OnRowBackgroundChanged(AvaloniaPropertyChangedEventArgs e) - { - foreach (DataGridRow row in GetAllRows()) - { - row.EnsureBackground(); - } - } - private void OnColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) { var value = (DataGridLength)e.NewValue; @@ -2060,6 +2058,25 @@ namespace Avalonia.Controls forceHorizontalScroll: true); } } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + if (DataConnection.DataSource != null && !DataConnection.EventsWired) + { + DataConnection.WireEvents(DataConnection.DataSource); + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + // When wired to INotifyCollectionChanged, the DataGrid will be cleaned up by GC + if (DataConnection.DataSource != null && DataConnection.EventsWired) + { + DataConnection.UnWireEvents(DataConnection.DataSource); + } + } /// /// Arranges the content of the . diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index e3f150f5c4..67183781d3 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -13,6 +13,7 @@ namespace Avalonia.Controls /// /// Represents an individual cell. /// + [TemplatePart(DATAGRIDCELL_elementRightGridLine, typeof(Rectangle))] [PseudoClasses(":selected", ":current", ":edited", ":invalid")] public class DataGridCell : ContentControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs index ccf1f3f77a..9826c15598 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs @@ -46,7 +46,7 @@ namespace Avalonia.Controls set => SetValue(IsThreeStateProperty, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index a77b482436..f3ea48ff80 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -192,14 +192,14 @@ namespace Avalonia.Controls set => SetValue(IsVisibleProperty, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsVisibleProperty) { OwningGrid?.OnColumnVisibleStateChanging(this); - var isVisible = (change as AvaloniaPropertyChangedEventArgs).NewValue.Value; + var isVisible = change.GetNewValue(); if (_headerCell != null) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs index 922b1d9c08..e7f9a9a6c4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs @@ -12,7 +12,8 @@ namespace Avalonia.Controls { internal class DataGridColumnCollection : ObservableCollection { - private DataGrid _owningGrid; + private readonly Dictionary _columnsMap = new Dictionary(); + private readonly DataGrid _owningGrid; public DataGridColumnCollection(DataGrid owningGrid) { @@ -124,18 +125,8 @@ namespace Avalonia.Controls internal int VisibleColumnCount { - get - { - int visibleColumnCount = 0; - for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++) - { - if (ItemsInternal[columnIndex].IsVisible) - { - visibleColumnCount++; - } - } - return visibleColumnCount; - } + get; + private set; } internal double VisibleEdgedColumnsWidth @@ -287,20 +278,31 @@ namespace Avalonia.Controls { VisibleStarColumnCount = 0; VisibleEdgedColumnsWidth = 0; + VisibleColumnCount = 0; + _columnsMap.Clear(); + for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++) { - if (ItemsInternal[columnIndex].IsVisible) + var item = ItemsInternal[columnIndex]; + _columnsMap[columnIndex] = item.DisplayIndex; + if (item.IsVisible) { - ItemsInternal[columnIndex].EnsureWidth(); - if (ItemsInternal[columnIndex].Width.IsStar) + VisibleColumnCount++; + item.EnsureWidth(); + if (item.Width.IsStar) { VisibleStarColumnCount++; } - VisibleEdgedColumnsWidth += ItemsInternal[columnIndex].ActualWidth; + VisibleEdgedColumnsWidth += item.ActualWidth; } } } + 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/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs index a4577ee952..52f0ad7537 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs @@ -444,12 +444,11 @@ namespace Avalonia.Controls // We need to explicitly collapse the cells of the invisible column because layout only goes through // visible ones - if (!updatedColumn.IsVisible) + ColumnHeaders?.InvalidateChildIndex(); + foreach (var row in GetAllRows()) { - foreach (DataGridRow row in GetAllRows()) - { - row.Cells[updatedColumn.Index].IsVisible = false; - } + row.Cells[updatedColumn.Index].IsVisible = updatedColumn.IsVisible; + row.InvalidateCellsIndex(); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs index fade597ca1..a3095ad214 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls private set; } - public int Count => GetCount(true); + public int Count => TryGetCount(true, false, out var count) ? count : 0; public bool DataIsPrimitive { @@ -193,22 +193,25 @@ namespace Avalonia.Controls } } - internal bool Any() - { - return GetCount(false) > 0; - } - /// When "allowSlow" is false, method will not use Linq.Count() method and will return 0 or 1 instead. - private int GetCount(bool allowSlow) + /// If "getAny" is true, method can use Linq.Any() method to speedup. + internal bool TryGetCount(bool allowSlow, bool getAny, out int count) { - return DataSource switch + bool result; + (result, count) = DataSource switch { - ICollection collection => collection.Count, - DataGridCollectionView cv => cv.Count, - IEnumerable enumerable when allowSlow => enumerable.Cast().Count(), - IEnumerable enumerable when !allowSlow => enumerable.Cast().Any() ? 1 : 0, - _ => 0 + ICollection collection => (true, collection.Count), + DataGridCollectionView cv => (true, cv.Count), + IEnumerable enumerable when allowSlow && !getAny => (true, enumerable.Cast().Count()), + IEnumerable enumerable when getAny => (true, enumerable.Cast().Any() ? 1 : 0), + _ => (false, 0) }; + return result; + } + + internal bool Any() + { + return TryGetCount(false, true, out var count) && count > 0; } /// @@ -383,7 +386,7 @@ namespace Avalonia.Controls List propertyNames = TypeHelper.SplitPropertyPath(propertyName); for (int i = 0; i < propertyNames.Count; i++) { - propertyInfo = propertyType.GetPropertyOrIndexer(propertyNames[i], out object[] index); + propertyInfo = propertyType.GetPropertyOrIndexer(propertyNames[i], out _); if (propertyInfo == null || propertyType.GetIsReadOnly() || propertyInfo.GetIsReadOnly()) { // Either the data type is read-only, the property doesn't exist, or it does exist but is read-only @@ -391,11 +394,10 @@ namespace Avalonia.Controls } // Check if EditableAttribute is defined on the property and if it indicates uneditable - object[] attributes = propertyInfo.GetCustomAttributes(typeof(EditableAttribute), true); + var attributes = propertyInfo.GetCustomAttributes(typeof(EditableAttribute), true); if (attributes != null && attributes.Length > 0) { - EditableAttribute editableAttribute = attributes[0] as EditableAttribute; - Debug.Assert(editableAttribute != null); + var editableAttribute = (EditableAttribute)attributes[0]; if (!editableAttribute.AllowEdit) { return true; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 1efce7c0b8..b062a14e39 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -21,6 +21,11 @@ namespace Avalonia.Controls /// /// Represents a row. /// + [TemplatePart(DATAGRIDROW_elementBottomGridLine, typeof(Rectangle))] + [TemplatePart(DATAGRIDROW_elementCells, typeof(DataGridCellsPresenter))] + [TemplatePart(DATAGRIDROW_elementDetails, typeof(DataGridDetailsPresenter))] + [TemplatePart(DATAGRIDROW_elementRoot, typeof(Panel))] + [TemplatePart(DATAGRIDROW_elementRowHeader, typeof(DataGridRowHeader))] [PseudoClasses(":selected", ":editing", ":invalid")] public class DataGridRow : TemplatedControl { @@ -543,7 +548,6 @@ namespace Avalonia.Controls RootElement = e.NameScope.Find(DATAGRIDROW_elementRoot); if (RootElement != null) { - EnsureBackground(); UpdatePseudoClasses(); } @@ -668,43 +672,9 @@ namespace Avalonia.Controls Slot = -1; } - // Make sure the row's background is set to its correct value. It could be explicity set or inherit - // DataGrid.RowBackground or DataGrid.AlternatingRowBackground - internal void EnsureBackground() + internal void InvalidateCellsIndex() { - // Inherit the DataGrid's RowBackground properties only if this row doesn't explicity have a background set - if (RootElement != null && OwningGrid != null) - { - IBrush newBackground = null; - if (Background == null) - { - if (Index % 2 == 0 || OwningGrid.AlternatingRowBackground == null) - { - // Use OwningGrid.RowBackground if the index is even or if the OwningGrid.AlternatingRowBackground is null - if (OwningGrid.RowBackground != null) - { - newBackground = OwningGrid.RowBackground; - } - } - else - { - // Alternate row - if (OwningGrid.AlternatingRowBackground != null) - { - newBackground = OwningGrid.AlternatingRowBackground; - } - } - } - else - { - newBackground = Background; - } - - if (RootElement.Background != newBackground) - { - RootElement.Background = newBackground; - } - } + _cellsElement?.InvalidateChildIndex(); } internal void EnsureFillerVisibility() @@ -1092,7 +1062,7 @@ namespace Avalonia.Controls } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == DataContextProperty) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 49ca23d34c..a3dfa44fc9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -14,6 +14,12 @@ using System.Reactive.Linq; namespace Avalonia.Controls { + [TemplatePart(DATAGRIDROWGROUPHEADER_expanderButton, typeof(ToggleButton))] + [TemplatePart(DATAGRIDROWGROUPHEADER_indentSpacer, typeof(Control))] + [TemplatePart(DATAGRIDROWGROUPHEADER_itemCountElement, typeof(TextBlock))] + [TemplatePart(DATAGRIDROWGROUPHEADER_propertyNameElement, typeof(TextBlock))] + [TemplatePart(DataGridRow.DATAGRIDROW_elementRoot, typeof(Panel))] + [TemplatePart(DataGridRow.DATAGRIDROW_elementRowHeader, typeof(DataGridRowHeader))] [PseudoClasses(":pressed", ":current", ":expanded")] public class DataGridRowGroupHeader : TemplatedControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 510072174f..03299bbf35 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -13,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents an individual row header. /// + [TemplatePart(DATAGRIDROWHEADER_elementRootName, typeof(Control))] [PseudoClasses(":invalid", ":selected", ":editing", ":current")] public class DataGridRowHeader : ContentControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index 1d5c899993..f3afe2c42d 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -5,6 +5,7 @@ using Avalonia.Collections; using Avalonia.Controls.Utils; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Utilities; using System; @@ -811,7 +812,7 @@ namespace Avalonia.Controls if (row.Slot > slotDeleted) { CorrectRowAfterDeletion(row, wasRow); - row.EnsureBackground(); + _rowsPresenter?.InvalidateChildIndex(row); } } @@ -867,7 +868,7 @@ namespace Avalonia.Controls if (row.Slot >= slotInserted) { DataGrid.CorrectRowAfterInsertion(row, rowInserted); - row.EnsureBackground(); + _rowsPresenter?.InvalidateChildIndex(row); } } @@ -1485,8 +1486,8 @@ namespace Avalonia.Controls // If the row has been recycled, reapply the BackgroundBrush if (row.IsRecycled) { - row.EnsureBackground(); row.ApplyCellsState(); + _rowsPresenter?.InvalidateChildIndex(row); } else if (row == EditingRow) { diff --git a/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs index 863910c226..bb8637cda2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs @@ -19,8 +19,6 @@ namespace Avalonia.Controls /// public class DataGridTextColumn : DataGridBoundColumn { - private const string DATAGRID_TextColumnCellTextBlockMarginKey = "DataGridTextColumnCellTextBlockMargin"; - /// /// Initializes a new instance of the class. /// @@ -121,7 +119,7 @@ namespace Avalonia.Controls set => SetValue(ForegroundProperty, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -178,8 +176,7 @@ namespace Avalonia.Controls { TextBlock textBlockElement = new TextBlock { - [!Layoutable.MarginProperty] = new DynamicResourceExtension(DATAGRID_TextColumnCellTextBlockMarginKey), - VerticalAlignment = VerticalAlignment.Center + Name = "CellTextBlock" }; SyncProperties(textBlockElement); diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index c5fe9f0cb2..e07c933039 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -3,12 +3,13 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Utilities; using System; +using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; -using Avalonia.Controls; -using Avalonia.Controls.Utils; namespace Avalonia.Controls.Primitives { @@ -16,9 +17,10 @@ namespace Avalonia.Controls.Primitives /// Used within the template of a /// to specify the location in the control's visual tree where the cells are to be added. /// - public sealed class DataGridCellsPresenter : Panel + public sealed class DataGridCellsPresenter : Panel, IChildIndexProvider { private double _fillerLeftEdge; + private EventHandler _childIndexChanged; // The desired height needs to be cached due to column virtualization; otherwise, the cells // would grow and shrink as the DataGrid scrolls horizontally @@ -42,6 +44,25 @@ namespace Avalonia.Controls.Primitives set; } + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return child is DataGridCell cell + ? OwningGrid.ColumnsInternal.GetColumnDisplayIndex(cell.ColumnIndex) + : throw new InvalidOperationException("Invalid cell type"); + } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + count = OwningGrid.ColumnsInternal.VisibleColumnCount; + return true; + } + /// /// Arranges the content of the . /// @@ -120,6 +141,13 @@ namespace Avalonia.Controls.Primitives } } + protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + { + base.ChildrenChanged(sender, e); + + InvalidateChildIndex(); + } + private static void EnsureCellDisplay(DataGridCell cell, bool displayColumn) { if (cell.IsCurrent) @@ -304,6 +332,11 @@ namespace Avalonia.Controls.Primitives DesiredHeight = 0; } + internal void InvalidateChildIndex() + { + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + } + private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge) { if (!column.IsVisible) diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index 4eed119240..108dc8ded7 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -3,8 +3,10 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using Avalonia.LogicalTree; using Avalonia.Media; using System; +using System.Collections.Specialized; using System.Diagnostics; namespace Avalonia.Controls.Primitives @@ -13,10 +15,11 @@ namespace Avalonia.Controls.Primitives /// Used within the template of a to specify the /// location in the control's visual tree where the column headers are to be added. /// - public sealed class DataGridColumnHeadersPresenter : Panel + public sealed class DataGridColumnHeadersPresenter : Panel, IChildIndexProvider { private Control _dragIndicator; private IControl _dropLocationIndicator; + private EventHandler _childIndexChanged; /// /// Tracks which column is currently being dragged. @@ -104,6 +107,25 @@ namespace Avalonia.Controls.Primitives set; } + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return child is DataGridColumnHeader header + ? OwningGrid.ColumnsInternal.GetColumnDisplayIndex(header.ColumnIndex) + : throw new InvalidOperationException("Invalid cell type"); + } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + count = OwningGrid.ColumnsInternal.VisibleColumnCount; + return true; + } + /// /// Arranges the content of the . /// @@ -391,5 +413,17 @@ namespace Avalonia.Controls.Primitives OwningGrid.ColumnsInternal.EnsureVisibleEdgedColumnsWidth(); return new Size(OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, height); } + + protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + { + base.ChildrenChanged(sender, e); + + InvalidateChildIndex(); + } + + internal void InvalidateChildIndex() + { + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + } } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index 308ebc69d4..5d82689eff 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -7,8 +7,8 @@ using System; using System.Diagnostics; using Avalonia.Input; -using Avalonia.Input.GestureRecognizers; using Avalonia.Layout; +using Avalonia.LogicalTree; using Avalonia.Media; namespace Avalonia.Controls.Primitives @@ -17,8 +17,10 @@ namespace Avalonia.Controls.Primitives /// Used within the template of a to specify the /// location in the control's visual tree where the rows are to be added. /// - public sealed class DataGridRowsPresenter : Panel + public sealed class DataGridRowsPresenter : Panel, IChildIndexProvider { + private EventHandler _childIndexChanged; + public DataGridRowsPresenter() { AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); @@ -44,6 +46,29 @@ namespace Avalonia.Controls.Primitives } } + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return child is DataGridRow row + ? row.Index + : throw new InvalidOperationException("Invalid DataGrid child"); + } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + return OwningGrid.DataConnection.TryGetCount(false, true, out count); + } + + internal void InvalidateChildIndex(DataGridRow row) + { + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row)); + } + /// /// Arranges the content of the . /// diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 8d4e327f3e..0d1fe43eb6 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -12,23 +12,32 @@ - - - - - + + + + + + + + @@ -102,22 +116,35 @@ + + + + + @@ -180,57 +188,58 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - + Fill="{TemplateBinding SeparatorBrush}" + IsVisible="{TemplateBinding AreSeparatorsVisible}" /> + + + + + - + @@ -271,38 +280,51 @@ - - - - - - - - + + + + + + + - + DataGridFrozenGrid.IsFrozen="True" /> + + - + + + + + @@ -430,9 +452,12 @@ Width="12" Height="12" Margin="12,0,0,0" + BorderBrush="{TemplateBinding BorderBrush}" + BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" - Foreground="{TemplateBinding Foreground}" - Focusable="False" /> + CornerRadius="{TemplateBinding CornerRadius}" + Focusable="False" + Foreground="{TemplateBinding Foreground}" /> - - + CornerRadius="{TemplateBinding CornerRadius}"> + diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 6486de132a..33e5efbc15 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -1,9 +1,17 @@ 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. @@ -28,12 +36,19 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDown.Value.set 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. @@ -44,10 +59,12 @@ MembersMustExist : Member 'public void Avalonia.Controls.TextBlock.SetFontSize(A 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. @@ -58,12 +75,17 @@ MembersMustExist : Member 'public System.Action Avalonia.Controls 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. @@ -87,4 +109,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor 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: 88 +Total Issues: 110 diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 3316c06bf5..5c95932c1f 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1346,12 +1346,16 @@ namespace Avalonia.Controls /// enabled. /// /// The property. - /// The new binding value for the property. - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + /// The current data binding state. + /// The current data binding error, if any. + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == TextProperty || property == SelectedItemProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 543a513d57..4d239e69f4 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -6,17 +6,12 @@ - - - - - - + diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 899521536f..0ef1ba4c8c 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -29,11 +29,14 @@ namespace Avalonia.Controls } /// - /// A button control. + /// A standard button control. /// - [PseudoClasses(":pressed")] - public class Button : ContentControl, ICommandSource + [PseudoClasses(pcFlyoutOpen, pcPressed)] + public class Button : ContentControl, ICommandSource, IClickableControl { + protected const string pcPressed = ":pressed"; + protected const string pcFlyoutOpen = ":flyout-open"; + /// /// Defines the property. /// @@ -92,6 +95,7 @@ namespace Avalonia.Controls private ICommand? _command; private bool _commandCanExecute = true; private KeyGesture? _hotkey; + private bool _isFlyoutOpen = false; /// /// Initializes static members of the class. @@ -107,7 +111,6 @@ namespace Avalonia.Controls /// public Button() { - UpdatePseudoClasses(IsPressed); } /// @@ -328,11 +331,30 @@ namespace Avalonia.Controls } } + /// + /// Opens the button's flyout. + /// protected virtual void OpenFlyout() { Flyout?.ShowAt(this); } + /// + /// Invoked when the button's flyout is opened. + /// + protected virtual void OnFlyoutOpened() + { + // Available for derived types + } + + /// + /// Invoked when the button's flyout is closed. + /// + protected virtual void OnFlyoutClosed() + { + // Available for derived types + } + /// protected override void OnPointerPressed(PointerPressedEventArgs e) { @@ -383,7 +405,15 @@ namespace Avalonia.Controls } /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + UnregisterFlyoutEvents(Flyout); + RegisterFlyoutEvents(Flyout); + UpdatePseudoClasses(); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -391,12 +421,13 @@ namespace Avalonia.Controls { if (((ILogical)this).IsAttachedToLogicalTree) { - if (change.OldValue.GetValueOrDefault() is ICommand oldCommand) + var (oldValue, newValue) = change.GetOldAndNewValue(); + if (oldValue is ICommand oldCommand) { oldCommand.CanExecuteChanged -= CanExecuteChanged; } - if (change.NewValue.GetValueOrDefault() is ICommand newCommand) + if (newValue is ICommand newCommand) { newCommand.CanExecuteChanged += CanExecuteChanged; } @@ -410,7 +441,7 @@ namespace Avalonia.Controls } else if (change.Property == IsCancelProperty) { - var isCancel = change.NewValue.GetValueOrDefault(); + var isCancel = change.GetNewValue(); if (VisualRoot is IInputElement inputRoot) { @@ -426,7 +457,7 @@ namespace Avalonia.Controls } else if (change.Property == IsDefaultProperty) { - var isDefault = change.NewValue.GetValueOrDefault(); + var isDefault = change.GetNewValue(); if (VisualRoot is IInputElement inputRoot) { @@ -442,29 +473,40 @@ namespace Avalonia.Controls } else if (change.Property == IsPressedProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(); } else if (change.Property == FlyoutProperty) { + var (oldFlyout, newFlyout) = change.GetOldAndNewValue(); + // If flyout is changed while one is already open, make sure we // close the old one first - if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout && + if (oldFlyout != null && oldFlyout.IsOpen) { oldFlyout.Hide(); } + + // Must unregister events here while a reference to the old flyout still exists + UnregisterFlyoutEvents(oldFlyout); + + RegisterFlyoutEvents(newFlyout); + UpdatePseudoClasses(); } } protected override AutomationPeer OnCreateAutomationPeer() => new ButtonAutomationPeer(this); /// - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { - base.UpdateDataValidation(property, value); + base.UpdateDataValidation(property, state, error); if (property == CommandProperty) { - if (value.Type == BindingValueType.BindingError) + if (state == BindingValueType.BindingError) { if (_commandCanExecute) { @@ -493,6 +535,32 @@ namespace Avalonia.Controls } } + /// + /// Registers all flyout events. + /// + /// The flyout to connect events to. + private void RegisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened += Flyout_Opened; + flyout.Closed += Flyout_Closed; + } + } + + /// + /// Explicitly unregisters all flyout events. + /// + /// The flyout to disconnect events from. + private void UnregisterFlyoutEvents(FlyoutBase? flyout) + { + if (flyout != null) + { + flyout.Opened -= Flyout_Opened; + flyout.Closed -= Flyout_Closed; + } + } + /// /// Starts listening for the Enter key when the button . /// @@ -560,11 +628,53 @@ namespace Avalonia.Controls /// /// Updates the visual state of the control by applying latest PseudoClasses. /// - private void UpdatePseudoClasses(bool isPressed) + private void UpdatePseudoClasses() { - PseudoClasses.Set(":pressed", isPressed); + PseudoClasses.Set(pcFlyoutOpen, _isFlyoutOpen); + PseudoClasses.Set(pcPressed, IsPressed); } void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + + void IClickableControl.RaiseClick() => OnClick(); + + /// + /// Event handler for when the button's flyout is opened. + /// + private void Flyout_Opened(object? sender, EventArgs e) + { + var flyout = sender as FlyoutBase; + + // It is possible to share flyouts among multiple controls including Button. + // This can cause a problem here since all controls that share a flyout receive + // the same Opened/Closed events at the same time. + // For Button that means they all would be updating their pseudoclasses accordingly. + // In other words, all Buttons with a shared Flyout would have the backgrounds changed together. + // To fix this, only continue here if the Flyout target matches this Button instance. + if (object.ReferenceEquals(flyout?.Target, this)) + { + _isFlyoutOpen = true; + UpdatePseudoClasses(); + + OnFlyoutOpened(); + } + } + + /// + /// Event handler for when the button's flyout is closed. + /// + private void Flyout_Closed(object? sender, EventArgs e) + { + var flyout = sender as FlyoutBase; + + // See comments in Flyout_Opened + if (object.ReferenceEquals(flyout?.Target, this)) + { + _isFlyoutOpen = false; + UpdatePseudoClasses(); + + OnFlyoutClosed(); + } + } } } diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 29a954098f..e455c6c6f3 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -210,13 +210,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ButtonSpinnerLocationProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs index 0ac2056ed1..0409eb30aa 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs @@ -540,11 +540,11 @@ namespace Avalonia.Controls } } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) { if (property == SelectedDateProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } diff --git a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs index eb90f6c399..7a5c74a51b 100644 --- a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs +++ b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs @@ -68,10 +68,7 @@ namespace Avalonia.Controls public static DateTime DiscardDayTime(DateTime d) { - int year = d.Year; - int month = d.Month; - DateTime newD = new DateTime(year, month, 1, 0, 0, 0); - return newD; + return new DateTime(d.Year, d.Month, 1, 0, 0, 0); } [return: NotNullIfNotNull("d")] diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index d4e7fc0e47..cbf9b35a05 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -184,6 +184,25 @@ namespace Avalonia.Controls this.UpdateSelectionBoxItem(SelectedItem); } + // Because the SelectedItem isn't connected to the visual tree + public override void InvalidateMirrorTransform() + { + base.InvalidateMirrorTransform(); + + if (SelectedItem is Control selectedControl) + { + selectedControl.InvalidateMirrorTransform(); + + foreach (var visual in selectedControl.GetVisualDescendants()) + { + if (visual is Control childControl) + { + childControl.InvalidateMirrorTransform(); + } + } + } + } + /// protected override void OnKeyDown(KeyEventArgs e) { @@ -434,42 +453,18 @@ namespace Avalonia.Controls private void SelectNext() { - int next = SelectedIndex + 1; - - if (next >= ItemCount) + if (ItemCount >= 1) { - if (WrapSelection == true) - { - next = 0; - } - else - { - return; - } + MoveSelection(NavigationDirection.Next, WrapSelection); } - - - - SelectedIndex = next; } private void SelectPrev() { - int prev = SelectedIndex - 1; - - if (prev < 0) + if (ItemCount >= 1) { - if (WrapSelection == true) - { - prev = ItemCount - 1; - } - else - { - return; - } + MoveSelection(NavigationDirection.Previous, WrapSelection); } - - SelectedIndex = prev; } } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index bc5195ff6c..ee2378101a 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -241,13 +241,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == WindowManagerAddShadowHintProperty && _popup != null) { - _popup.WindowManagerAddShadowHint = change.NewValue.GetValueOrDefault(); + _popup.WindowManagerAddShadowHint = change.GetNewValue(); } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 45c0d2948d..d6a5fa0727 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -160,6 +160,16 @@ namespace Avalonia.Controls /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; + /// + /// Gets a value indicating whether control bypass FlowDirecton policies. + /// + /// + /// Related to FlowDirection system and returns false as default, so if + /// is RTL then control will get a mirror presentation. + /// For controls that want to avoid this behavior, override this property and return true. + /// + protected virtual bool BypassFlowDirectionPolicies => false; + /// void ISetterValue.Initialize(ISetter setter) { @@ -219,6 +229,14 @@ namespace Avalonia.Controls base.OnDetachedFromVisualTreeCore(e); } + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + InvalidateMirrorTransform(); + } + /// protected override void OnGotFocus(GotFocusEventArgs e) { @@ -329,5 +347,55 @@ namespace Avalonia.Controls } } } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == FlowDirectionProperty) + { + InvalidateMirrorTransform(); + + foreach (var visual in VisualChildren) + { + if (visual is Control child) + { + child.InvalidateMirrorTransform(); + } + } + } + } + + /// + /// Computes the value according to the + /// and + /// + public virtual void InvalidateMirrorTransform() + { + var flowDirection = this.FlowDirection; + var parentFlowDirection = FlowDirection.LeftToRight; + + bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies; + bool parentBypassFlowDirectionPolicies = false; + + var parent = this.FindAncestorOfType(); + if (parent != null) + { + parentFlowDirection = parent.FlowDirection; + parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies; + } + else if (Parent is Control logicalParent) + { + parentFlowDirection = logicalParent.FlowDirection; + parentBypassFlowDirectionPolicies = logicalParent.BypassFlowDirectionPolicies; + } + + bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies; + bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies; + + bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored; + + HasMirrorTransform = shouldApplyMirrorTransform; + } } } diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs index a923c44d05..667f994a1d 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -1,7 +1,9 @@ using System; using System.Globalization; using System.Linq; +using Avalonia.Controls.Presenters; using Avalonia.Input; +using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.VisualTree; @@ -60,6 +62,7 @@ namespace Avalonia.Controls.Primitives private Vector _offset; private bool _hasInit; private bool _suppressUpdateOffset; + private ScrollContentPresenter? _parentScroller; public DateTimePickerPanel() { @@ -255,6 +258,8 @@ namespace Avalonia.Controls.Primitives _suppressUpdateOffset = true; SelectedValue = (int)newSel * Increment + MinimumValue; _suppressUpdateOffset = false; + + System.Diagnostics.Debug.WriteLine($"Offset: {_offset} ItemHeight: {ItemHeight}"); } } @@ -270,7 +275,7 @@ namespace Avalonia.Controls.Primitives public Size Extent => _extent; - public Size Viewport => new Size(0, ItemHeight); + public Size Viewport => Bounds.Size; public event EventHandler? ScrollInvalidated; @@ -341,6 +346,20 @@ namespace Avalonia.Controls.Primitives return finalSize; } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _parentScroller = this.GetVisualParent() as ScrollContentPresenter; + _parentScroller?.AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _parentScroller?.RemoveHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + _parentScroller = null; + } + protected override void OnKeyDown(KeyEventArgs e) { switch (e.Key) @@ -554,5 +573,15 @@ namespace Avalonia.Controls.Primitives { ScrollInvalidated?.Invoke(this, e); } + + private void OnScrollGestureEnded(object? sender, ScrollGestureEndedEventArgs e) + { + var snapY = Math.Round(Offset.Y / ItemHeight) * ItemHeight; + + if (snapY != Offset.Y) + { + Offset = Offset.WithY(snapY); + } + } } } diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs index 5b63f95432..fdd78459c8 100644 --- a/src/Avalonia.Controls/Documents/Inline.cs +++ b/src/Avalonia.Controls/Documents/Inline.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls.Documents TextDecorations, Foreground, Background, BaselineAlignment); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs index a7dd5fd94f..2f9ba013ed 100644 --- a/src/Avalonia.Controls/Documents/Run.cs +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -71,7 +71,7 @@ namespace Avalonia.Controls.Documents return text.Length; } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs index d8e13554b5..450aafbfaf 100644 --- a/src/Avalonia.Controls/Documents/TextElement.cs +++ b/src/Avalonia.Controls/Documents/TextElement.cs @@ -256,7 +256,7 @@ namespace Avalonia.Controls.Documents /// public event EventHandler? Invalidated; - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs deleted file mode 100644 index 4e17f5bff5..0000000000 --- a/src/Avalonia.Controls/DropDown.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Avalonia.Logging; -using Avalonia.Styling; - -namespace Avalonia.Controls -{ - [Obsolete("Use ComboBox")] - public class DropDown : ComboBox, IStyleable - { - public DropDown() - { - Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "DropDown is deprecated: Use ComboBox"); - } - - Type IStyleable.StyleKey => typeof(ComboBox); - } - - [Obsolete("Use ComboBoxItem")] - public class DropDownItem : ComboBoxItem, IStyleable - { - public DropDownItem() - { - Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "DropDownItem is deprecated: Use ComboBoxItem"); - } - - Type IStyleable.StyleKey => typeof(ComboBoxItem); - } -} diff --git a/src/Avalonia.Controls/DropDownButton.cs b/src/Avalonia.Controls/DropDownButton.cs new file mode 100644 index 0000000000..8dc40f42af --- /dev/null +++ b/src/Avalonia.Controls/DropDownButton.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Controls +{ + /// + /// A button with an added drop-down chevron to visually indicate it has a flyout with additional actions. + /// + public class DropDownButton : Button + { + /// + /// Initializes a new instance of the class. + /// + public DropDownButton() + { + } + } +} diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 020b162864..3ba99d8a67 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -106,13 +106,13 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ExpandDirectionProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index da13416700..eb60fca367 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -15,6 +15,7 @@ namespace Avalonia.Controls.Generators /// public class TreeContainerIndex { + private readonly Dictionary> _itemToContainerSet = new Dictionary>(); private readonly Dictionary _itemToContainer = new Dictionary(); private readonly Dictionary _containerToItem = new Dictionary(); @@ -45,14 +46,45 @@ namespace Avalonia.Controls.Generators /// The item container. public void Add(object item, IControl container) { - _itemToContainer.Add(item, container); + _itemToContainer[item] = container; + if (_itemToContainerSet.TryGetValue(item, out var set)) + { + set.Add(container); + } + else + { + _itemToContainerSet.Add(item, new HashSet { container }); + } + _containerToItem.Add(container, item); Materialized?.Invoke( - this, + this, new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } + /// + /// Removes a container from private collections. + /// + /// The item container. + /// The DataContext object + private void RemoveContainer(IControl container, object item) + { + if (_itemToContainerSet.TryGetValue(item, out var set)) + { + set.Remove(container); + if (set.Count == 0) + { + _itemToContainerSet.Remove(item); + _itemToContainer.Remove(item); + } + else + { + _itemToContainer[item] = set.First(); + } + } + } + /// /// Removes a container from the index. /// @@ -61,10 +93,10 @@ namespace Avalonia.Controls.Generators { var item = _containerToItem[container]; _containerToItem.Remove(container); - _itemToContainer.Remove(item); + RemoveContainer(container, item); Dematerialized?.Invoke( - this, + this, new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } @@ -79,7 +111,7 @@ namespace Avalonia.Controls.Generators { var item = _containerToItem[container.ContainerControl]; _containerToItem.Remove(container.ContainerControl); - _itemToContainer.Remove(item); + RemoveContainer(container.ContainerControl, item); } Dematerialized?.Invoke( @@ -97,6 +129,14 @@ namespace Avalonia.Controls.Generators if (item != null) { _itemToContainer.TryGetValue(item, out var result); + if (result == null) + { + _itemToContainerSet.TryGetValue(item, out var set); + if (set?.Count > 0) + { + return set.FirstOrDefault(); + } + } return result; } @@ -113,6 +153,10 @@ namespace Avalonia.Controls.Generators if (container != null) { _containerToItem.TryGetValue(container, out var result); + if (result != null) + { + _itemToContainer[result] = container; + } return result; } diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index d83b996aa0..bde1da509b 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -12,21 +12,61 @@ namespace Avalonia.Controls class HotkeyCommandWrapper : ICommand { - public HotkeyCommandWrapper(ICommandSource? control) + readonly WeakReference reference; + + public HotkeyCommandWrapper(IControl control) { - CommandSource = control; + reference = new WeakReference(control); } - public readonly ICommandSource? CommandSource; + public ICommand? GetCommand() + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + return command; + } + else if (target is IClickableControl { }) + { + return this; + } + } + return null; + } - private ICommand? GetCommand() => CommandSource?.Command; + public bool CanExecute(object? parameter) + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + return commandSource.IsEffectivelyEnabled + && command.CanExecute(commandSource.CommandParameter) == true; + } + else if (target is IClickableControl clickable) + { + return clickable.IsEffectivelyEnabled; + } + } + return false; + } - public bool CanExecute(object? parameter) => - CommandSource?.Command?.CanExecute(CommandSource.CommandParameter) == true - && CommandSource.IsEffectivelyEnabled; + public void Execute(object? parameter) + { + if (reference.Target is { } target) + { + if (target is ICommandSource commandSource && commandSource.Command is { } command) + { + command.Execute(commandSource.CommandParameter); + } + else if (target is IClickableControl { IsEffectivelyEnabled: true } clickable) + { + clickable.RaiseClick(); + } + } + } - public void Execute(object? parameter) => - GetCommand()?.Execute(CommandSource?.CommandParameter); #pragma warning disable 67 // Event not used public event EventHandler? CanExecuteChanged; @@ -47,7 +87,7 @@ namespace Avalonia.Controls public Manager(IControl control) { _control = control; - _wrapper = new HotkeyCommandWrapper(_control as ICommandSource); + _wrapper = new HotkeyCommandWrapper(_control); } public void Init() @@ -104,13 +144,14 @@ namespace Avalonia.Controls { HotKeyProperty.Changed.Subscribe(args => { - if (args.NewValue.Value is null) return; + if (args.NewValue.Value is null) + return; var control = args.Sender as IControl; - if (control is not ICommandSource) + if (control is not IClickableControl) { Logging.Logger.TryGet(Logging.LogEventLevel.Warning, Logging.LogArea.Control)?.Log(control, - $"The element {args.Sender.GetType().Name} does not implement ICommandSource and does not support binding a HotKey ({args.NewValue})."); + $"The element {args.Sender.GetType().Name} does not implement IClickableControl and does not support binding a HotKey ({args.NewValue})."); return; } diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 3d67880638..7408bff902 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -66,6 +66,8 @@ namespace Avalonia.Controls set { SetValue(StretchDirectionProperty, value); } } + protected override bool BypassFlowDirectionPolicies => true; + /// /// Renders the control. /// diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 0cd72dc91c..256160a116 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -166,7 +166,7 @@ namespace Avalonia.Controls if (Presenter is IChildIndexProvider innerProvider) { innerProvider.ChildIndexChanged += PresenterChildIndexChanged; - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); } } @@ -341,13 +341,13 @@ namespace Avalonia.Controls return new ItemsControlAutomationPeer(this); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ItemCountProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } @@ -508,7 +508,6 @@ namespace Avalonia.Controls do { result = container.GetControl(direction, c, wrap); - from = from ?? result; if (result != null && result.Focusable && diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index ad64c61ebe..933788f9ea 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -280,7 +280,7 @@ namespace Avalonia.Controls base.OnLostFocus(e); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { void UpdateMaskProvider() { diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index cddd621b8e..619eafb71b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls /// [TemplatePart("PART_Popup", typeof(Popup))] [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")] - public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource + public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource, IClickableControl { /// /// Defines the property. @@ -501,12 +501,15 @@ namespace Avalonia.Controls return new MenuItemAutomationPeer(this); } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { - base.UpdateDataValidation(property, value); + base.UpdateDataValidation(property, state, error); if (property == CommandProperty) { - _commandBindingError = value.Type == BindingValueType.BindingError; + _commandBindingError = state == BindingValueType.BindingError; if (_commandBindingError && _commandCanExecute) { _commandCanExecute = false; @@ -644,7 +647,9 @@ namespace Avalonia.Controls /// The property change event. private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e) { - if ((bool)e.NewValue!) + var parentMenu = Parent as Menu; + + if ((bool)e.NewValue! && (parentMenu is null || parentMenu.IsOpen)) { Focus(); } @@ -705,6 +710,14 @@ namespace Avalonia.Controls void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); + void IClickableControl.RaiseClick() + { + if (IsEffectivelyEnabled) + { + RaiseEvent(new RoutedEventArgs(ClickEvent)); + } + } + /// /// A dependency resolver which returns a . /// diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index d6b82a8f8a..2449f4c15c 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -139,13 +139,13 @@ namespace Avalonia.Controls.Notifications notificationControl.Close(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == PositionProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index fbbaab6182..4d86a0f17c 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -403,12 +403,16 @@ namespace Avalonia.Controls /// enabled. /// /// The property. - /// The new binding value for the property. - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + /// The current data binding state. + /// The current data binding error, if any. + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == TextProperty || property == ValueProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 482a7fab84..2230b4b0d2 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -147,7 +147,7 @@ namespace Avalonia.Controls throw new NotSupportedException(); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); InvalidateMeasureOnChildrenChanged(); } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 23ecdb2e7b..6e9ac537f1 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -149,13 +149,18 @@ namespace Avalonia.Controls.Platform case Key.Up: case Key.Down: { - if (item?.IsTopLevel == true) + if (item?.IsTopLevel == true && item.HasSubMenu) { - if (item.HasSubMenu && !item.IsSubMenuOpen) + if (!item.IsSubMenuOpen) { Open(item, true); - e.Handled = true; } + else + { + item.MoveSelection(NavigationDirection.First, true); + } + + e.Handled = true; } else { @@ -247,7 +252,8 @@ namespace Avalonia.Controls.Platform // new menu. if (item.IsSubMenuOpen && item.Parent is IMenu && - item.Parent.SelectedItem is object) + item.Parent.SelectedItem is object && + item.Parent.SelectedItem != item) { item.Close(); Open(item.Parent.SelectedItem, true); diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index ae08b4a452..e70a3bc1c9 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -1,5 +1,6 @@ using System; +using Avalonia.Controls.Documents; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -47,7 +48,73 @@ namespace Avalonia.Controls.Presenters /// public static readonly StyledProperty BoxShadowProperty = Border.BoxShadowProperty.AddOwner(); - + + /// + /// Defines the property. + /// + public static readonly AttachedProperty ForegroundProperty = + TextElement.ForegroundProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontFamilyProperty = + TextElement.FontFamilyProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontSizeProperty = + TextElement.FontSizeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStyleProperty = + TextElement.FontStyleProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontWeightProperty = + TextElement.FontWeightProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty TextAlignmentProperty = + TextBlock.TextAlignmentProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty TextWrappingProperty = + TextBlock.TextWrappingProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty TextTrimmingProperty = + TextBlock.TextTrimmingProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty LineHeightProperty = + TextBlock.LineHeightProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty MaxLinesProperty = + TextBlock.MaxLinesProperty.AddOwner(); + /// /// Defines the property. /// @@ -160,6 +227,105 @@ namespace Avalonia.Controls.Presenters set => SetValue(BoxShadowProperty, value); } + /// + /// Gets or sets a brush used to paint the text. + /// + public IBrush? Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + /// + /// Gets or sets the font family. + /// + public FontFamily FontFamily + { + get => GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get => GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + /// + /// Gets or sets the font style. + /// + public FontStyle FontStyle + { + get => GetValue(FontStyleProperty); + set => SetValue(FontStyleProperty, value); + } + + /// + /// Gets or sets the font weight. + /// + public FontWeight FontWeight + { + get => GetValue(FontWeightProperty); + set => SetValue(FontWeightProperty, value); + } + + /// + /// Gets or sets the font stretch. + /// + public FontStretch FontStretch + { + get => GetValue(FontStretchProperty); + set => SetValue(FontStretchProperty, value); + } + + /// + /// Gets or sets the text alignment + /// + public TextAlignment TextAlignment + { + get => GetValue(TextAlignmentProperty); + set => SetValue(TextAlignmentProperty, value); + } + + /// + /// Gets or sets the text wrapping + /// + public TextWrapping TextWrapping + { + get => GetValue(TextWrappingProperty); + set => SetValue(TextWrappingProperty, value); + } + + /// + /// Gets or sets the text trimming + /// + public TextTrimming TextTrimming + { + get => GetValue(TextTrimmingProperty); + set => SetValue(TextTrimmingProperty, value); + } + + /// + /// Gets or sets the line height + /// + public double LineHeight + { + get => GetValue(LineHeightProperty); + set => SetValue(LineHeightProperty, value); + } + + /// + /// Gets or sets the max lines + /// + public int MaxLines + { + get => GetValue(MaxLinesProperty); + set => SetValue(MaxLinesProperty, value); + } + /// /// Gets the control displayed by the presenter. /// @@ -238,7 +404,7 @@ namespace Avalonia.Controls.Presenters } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); switch (change.Property.Name) diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index f938c8d437..2821fa8cf0 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -158,7 +158,7 @@ namespace Avalonia.Controls.Presenters { ItemsChanged(e); - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 97fb4c3f43..c526b7ac49 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -389,7 +389,7 @@ namespace Avalonia.Controls.Presenters { var logicalUnits = delta.Y / logicalScrollItemSize.Y; delta = delta.WithY(delta.Y - logicalUnits * logicalScrollItemSize.Y); - dy = logicalUnits * scrollable!.ScrollSize.Height; + dy = logicalUnits; } else dy = delta.Y; @@ -407,7 +407,7 @@ namespace Avalonia.Controls.Presenters { var logicalUnits = delta.X / logicalScrollItemSize.X; delta = delta.WithX(delta.X - logicalUnits * logicalScrollItemSize.X); - dx = logicalUnits * scrollable!.ScrollSize.Width; + dx = logicalUnits; } else dx = delta.X; @@ -469,7 +469,7 @@ namespace Avalonia.Controls.Presenters } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == OffsetProperty && !_arranging) { diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index a6a0fc7bc5..d127866640 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -282,6 +282,8 @@ namespace Avalonia.Controls.Presenters } } + protected override bool BypassFlowDirectionPolicies => true; + /// /// Creates the used to render the text. /// @@ -774,7 +776,7 @@ namespace Avalonia.Controls.Presenters _caretTimer.Tick -= CaretTimerTick; } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 353f12118f..fb047d93df 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -69,15 +69,18 @@ namespace Avalonia.Controls.Primitives { foreach (var child in Children) { - var info = child.GetValue(s_adornedElementInfoProperty); - - if (info != null && info.Bounds.HasValue) - { - child.Measure(info.Bounds.Value.Bounds.Size); - } - else + if (child is AvaloniaObject ao) { - child.Measure(availableSize); + var info = ao.GetValue(s_adornedElementInfoProperty); + + if (info != null && info.Bounds.HasValue) + { + child.Measure(info.Bounds.Value.Bounds.Size); + } + else + { + child.Measure(availableSize); + } } } @@ -88,19 +91,22 @@ namespace Avalonia.Controls.Primitives { foreach (var child in Children) { - var info = child.GetValue(s_adornedElementInfoProperty); - var isClipEnabled = child.GetValue(IsClipEnabledProperty); - - if (info != null && info.Bounds.HasValue) - { - child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform); - child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); - UpdateClip(child, info.Bounds.Value, isClipEnabled); - child.Arrange(info.Bounds.Value.Bounds); - } - else + if (child is AvaloniaObject ao) { - child.Arrange(new Rect(finalSize)); + var info = ao.GetValue(s_adornedElementInfoProperty); + var isClipEnabled = ao.GetValue(IsClipEnabledProperty); + + if (info != null && info.Bounds.HasValue) + { + child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform); + child.RenderTransformOrigin = new RelativePoint(new Point(0, 0), RelativeUnit.Absolute); + UpdateClip(child, info.Bounds.Value, isClipEnabled); + child.Arrange(info.Bounds.Value.Bounds); + } + else + { + child.Arrange(new Rect(finalSize)); + } } } diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 6251d5cda7..6ac544e0fe 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -76,7 +76,7 @@ namespace Avalonia.Controls.Primitives Rect? rect = null) { _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot()!, target, placement, offset, anchor, - gravity, constraintAdjustment, rect); + gravity, constraintAdjustment, rect, FlowDirection); UpdatePosition(); } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 340076a407..8daf1ac68a 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -46,6 +46,7 @@ Copyright © 2019 Nikita Tsukanov using System; using Avalonia.VisualTree; +using Avalonia.Media; namespace Avalonia.Controls.Primitives.PopupPositioning { @@ -444,7 +445,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning TopLevel topLevel, IVisual target, PlacementMode placement, Point offset, PopupAnchor anchor, PopupGravity gravity, - PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect) + PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect, + FlowDirection flowDirection) { // We need a better way for tracking the last pointer position #pragma warning disable CS0618 // Type or member is obsolete @@ -503,6 +505,32 @@ namespace Avalonia.Controls.Primitives.PopupPositioning else throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); } + + // Invert coordinate system if FlowDirection is RTL + if (flowDirection == FlowDirection.RightToLeft) + { + if ((positionerParameters.Anchor & PopupAnchor.Right) == PopupAnchor.Right) + { + positionerParameters.Anchor ^= PopupAnchor.Right; + positionerParameters.Anchor |= PopupAnchor.Left; + } + else if ((positionerParameters.Anchor & PopupAnchor.Left) == PopupAnchor.Left) + { + positionerParameters.Anchor ^= PopupAnchor.Left; + positionerParameters.Anchor |= PopupAnchor.Right; + } + + if ((positionerParameters.Gravity & PopupGravity.Right) == PopupGravity.Right) + { + positionerParameters.Gravity ^= PopupGravity.Right; + positionerParameters.Gravity |= PopupGravity.Left; + } + else if ((positionerParameters.Gravity & PopupGravity.Left) == PopupGravity.Left) + { + positionerParameters.Gravity ^= PopupGravity.Left; + positionerParameters.Gravity |= PopupGravity.Right; + } + } } } diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 99ef93d8e8..abf56e5420 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -93,7 +93,7 @@ namespace Avalonia.Controls.Primitives Rect? rect = null) { _positionerParameters.ConfigurePosition(ParentTopLevel, target, - placement, offset, anchor, gravity, constraintAdjustment, rect); + placement, offset, anchor, gravity, constraintAdjustment, rect, FlowDirection); if (_positionerParameters.Size != default) UpdatePosition(); diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 6a30097fbb..e5c3392faf 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -194,13 +194,13 @@ namespace Avalonia.Controls.Primitives } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == OrientationProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } else if (change.Property == AllowAutoHideProperty) { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index cec02c7ae9..bff6799792 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -501,12 +501,16 @@ namespace Avalonia.Controls.Primitives /// enabled. /// /// The property. - /// The new binding value for the property. - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + /// The current data binding state. + /// The current data binding error, if any. + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == SelectedItemProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } @@ -533,9 +537,9 @@ namespace Avalonia.Controls.Primitives bool Match(ItemContainerInfo info) { - if (info.ContainerControl.IsSet(TextSearch.TextProperty)) + if (info.ContainerControl is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty)) { - var searchText = info.ContainerControl.GetValue(TextSearch.TextProperty); + var searchText = ao.GetValue(TextSearch.TextProperty); if (searchText?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true) { @@ -585,7 +589,7 @@ namespace Avalonia.Controls.Primitives } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -595,7 +599,7 @@ namespace Avalonia.Controls.Primitives } if (change.Property == ItemsProperty && _updateState is null && _selection is object) { - var newValue = change.NewValue.GetValueOrDefault(); + var newValue = change.GetNewValue(); _selection.Source = newValue; if (newValue is null) @@ -605,7 +609,7 @@ namespace Avalonia.Controls.Primitives } else if (change.Property == SelectionModeProperty && _selection is object) { - var newValue = change.NewValue.GetValueOrDefault(); + var newValue = change.GetNewValue(); _selection.SingleSelect = !newValue.HasAllFlags(SelectionMode.Multiple); } else if (change.Property == WrapSelectionProperty) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 795c307c54..6e4ae748d9 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -356,6 +356,11 @@ namespace Avalonia.Controls.Primitives base.OnDetachedFromLogicalTree(e); } + /// + /// Called when the control's template is applied. + /// In simple terms, this means the method is called just before the control is displayed. + /// + /// The event args. protected virtual void OnApplyTemplate(TemplateAppliedEventArgs e) { } diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index f8d6046101..49f0cda982 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -291,13 +291,13 @@ namespace Avalonia.Controls.Primitives return arrangeSize; } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == OrientationProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index a4f2cc799a..1075328c67 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -178,17 +178,17 @@ namespace Avalonia.Controls return base.ArrangeOverride(finalSize); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsIndeterminateProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault(), null); + UpdatePseudoClasses(change.GetNewValue(), null); } else if (change.Property == OrientationProperty) { - UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(null, change.GetNewValue()); } } diff --git a/src/Avalonia.Controls/RepeatButton.cs b/src/Avalonia.Controls/RepeatButton.cs index 0415a78721..80f841fa18 100644 --- a/src/Avalonia.Controls/RepeatButton.cs +++ b/src/Avalonia.Controls/RepeatButton.cs @@ -70,11 +70,11 @@ namespace Avalonia.Controls _repeatTimer?.Stop(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); - if (change.Property == IsPressedProperty && change.NewValue.GetValueOrDefault() == false) + if (change.Property == IsPressedProperty && change.GetNewValue() == false) { StopTimer(); } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 09c0e58332..3f42d95deb 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber + public class ItemsRepeater : Panel, IChildIndexProvider { /// /// Defines the property. @@ -60,6 +60,7 @@ namespace Avalonia.Controls private readonly ViewManager _viewManager; private readonly ViewportManager _viewportManager; + private readonly TargetWeakEventSubscriber _layoutWeakSubscriber; private IEnumerable? _items; private VirtualizingLayoutContext? _layoutContext; private EventHandler? _childIndexChanged; @@ -74,6 +75,15 @@ namespace Avalonia.Controls /// public ItemsRepeater() { + _layoutWeakSubscriber = new TargetWeakEventSubscriber( + this, static (target, _, ev, _) => + { + if (ev == AttachedLayout.ArrangeInvalidatedWeakEvent) + target.InvalidateArrange(); + else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) + target.InvalidateMeasure(); + }); + _viewManager = new ViewManager(this); _viewportManager = new ViewportManager(this); KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once); @@ -257,10 +267,9 @@ namespace Avalonia.Controls internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false); - internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element) + internal static VirtualizationInfo? TryGetVirtualizationInfo(IControl element) { - var value = element.GetValue(VirtualizationInfoProperty); - return value; + return (element as AvaloniaObject)?.GetValue(VirtualizationInfoProperty); } internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element) @@ -277,15 +286,20 @@ namespace Avalonia.Controls internal static VirtualizationInfo GetVirtualizationInfo(IControl element) { - var result = element.GetValue(VirtualizationInfoProperty); - - if (result == null) + if (element is AvaloniaObject ao) { - result = new VirtualizationInfo(); - element.SetValue(VirtualizationInfoProperty, result); + var result = ao.GetValue(VirtualizationInfoProperty); + + if (result == null) + { + result = new VirtualizationInfo(); + ao.SetValue(VirtualizationInfoProperty, result); + } + + return result; } - return result; + throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported."); } private protected override void InvalidateMeasureOnChildrenChanged() @@ -391,11 +405,7 @@ namespace Avalonia.Controls var newBounds = element.Bounds; virtInfo.ArrangeBounds = newBounds; - if (!virtInfo.IsRegisteredAsAnchorCandidate) - { - _viewportManager.RegisterScrollAnchorCandidate(element); - virtInfo.IsRegisteredAsAnchorCandidate = true; - } + _viewportManager.RegisterScrollAnchorCandidate(element, virtInfo); } } @@ -420,12 +430,11 @@ namespace Avalonia.Controls _viewportManager.ResetScrollers(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == ItemsProperty) { - var oldEnumerable = change.OldValue.GetValueOrDefault(); - var newEnumerable = change.NewValue.GetValueOrDefault(); + var (oldEnumerable, newEnumerable) = change.GetOldAndNewValue(); if (oldEnumerable != newEnumerable) { @@ -440,23 +449,21 @@ namespace Avalonia.Controls } else if (change.Property == ItemTemplateProperty) { - OnItemTemplateChanged( - change.OldValue.GetValueOrDefault(), - change.NewValue.GetValueOrDefault()); + var (oldvalue, newValue) = change.GetOldAndNewValue(); + OnItemTemplateChanged(oldvalue, newValue); } else if (change.Property == LayoutProperty) { - OnLayoutChanged( - change.OldValue.GetValueOrDefault(), - change.NewValue.GetValueOrDefault()); + var (oldvalue, newValue) = change.GetOldAndNewValue(); + OnLayoutChanged(oldvalue, newValue); } else if (change.Property == HorizontalCacheLengthProperty) { - _viewportManager.HorizontalCacheLength = change.NewValue.GetValueOrDefault(); + _viewportManager.HorizontalCacheLength = change.GetNewValue(); } else if (change.Property == VerticalCacheLengthProperty) { - _viewportManager.VerticalCacheLength = change.NewValue.GetValueOrDefault(); + _viewportManager.VerticalCacheLength = change.GetNewValue(); } base.OnPropertyChanged(change); @@ -480,7 +487,7 @@ namespace Avalonia.Controls _processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset); _viewManager.ClearElement(element, isClearedDueToCollectionChange); - _viewportManager.OnElementCleared(element); + _viewportManager.OnElementCleared(element, GetVirtualizationInfo(element)); } private int GetElementIndexImpl(IControl element) @@ -491,7 +498,7 @@ namespace Avalonia.Controls if (parent == this) { var virtInfo = TryGetVirtualizationInfo(element); - return _viewManager.GetElementIndex(virtInfo); + return _viewManager.GetElementIndex(virtInfo!); } return -1; @@ -728,8 +735,8 @@ namespace Avalonia.Controls { oldValue.UninitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, _layoutWeakSubscriber); // Walk through all the elements and make sure they are cleared foreach (var element in Children) @@ -747,8 +754,8 @@ namespace Avalonia.Controls { newValue.InitializeForContext(LayoutContext); - AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this); - AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this); + AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); + AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); } bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; @@ -798,15 +805,7 @@ namespace Avalonia.Controls { _viewportManager.OnBringIntoViewRequested(e); } - - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, EventArgs e) - { - if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent) - InvalidateArrange(); - else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent) - InvalidateMeasure(); - } - + private VirtualizingLayoutContext GetLayoutContext() { if (_layoutContext == null) diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs index 9e2da81f99..cf2b40836e 100644 --- a/src/Avalonia.Controls/Repeater/RecyclePool.cs +++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs @@ -80,8 +80,8 @@ namespace Avalonia.Controls return null; } - internal string GetReuseKey(IControl element) => element.GetValue(ReuseKeyProperty); - internal void SetReuseKey(IControl element, string value) => element.SetValue(ReuseKeyProperty, value); + internal string GetReuseKey(IControl element) => ((Control)element).GetValue(ReuseKeyProperty); + internal void SetReuseKey(IControl element, string value) => ((Control)element).SetValue(ReuseKeyProperty, value); private IPanel? EnsureOwnerIsPanelOrNull(IControl? owner) { diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index b28753b518..12db48a2c2 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls if (madeAnchor != null) { var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor); - if (anchorVirtInfo.Index == index) + if (anchorVirtInfo!.Index == index) { element = madeAnchor; } @@ -60,12 +60,12 @@ namespace Avalonia.Controls var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (suppressAutoRecycle) { - virtInfo.AutoRecycleCandidate = false; + virtInfo!.AutoRecycleCandidate = false; Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} Not AutoRecycleCandidate:", virtInfo.Index); } else { - virtInfo.AutoRecycleCandidate = true; + virtInfo!.AutoRecycleCandidate = true; virtInfo.KeepAlive = true; Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} AutoRecycleCandidate:", virtInfo.Index); } diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index ec25fcb265..7f03cc575e 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -249,9 +249,10 @@ namespace Avalonia.Controls virtInfo.IsRegisteredAsAnchorCandidate = false; } - public void OnElementCleared(IControl element) + public void OnElementCleared(IControl element, VirtualizationInfo virtInfo) { _scroller?.UnregisterAnchorCandidate(element); + virtInfo.IsRegisteredAsAnchorCandidate = false; } public void OnOwnerMeasuring() @@ -358,9 +359,12 @@ namespace Avalonia.Controls { foreach (var child in _owner.Children) { - if (child != targetChild) + var info = ItemsRepeater.GetVirtualizationInfo(child); + + if (child != targetChild && info.IsRegisteredAsAnchorCandidate) { _scroller.UnregisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = false; } } } @@ -377,9 +381,13 @@ namespace Avalonia.Controls } } - public void RegisterScrollAnchorCandidate(IControl element) + public void RegisterScrollAnchorCandidate(IControl element, VirtualizationInfo virtInfo) { - _scroller?.RegisterAnchorCandidate(element); + if (!virtInfo.IsRegisteredAsAnchorCandidate) + { + _scroller?.RegisterAnchorCandidate(element); + virtInfo.IsRegisteredAsAnchorCandidate = true; + } } private IControl? GetImmediateChildOfRepeater(IControl descendant) @@ -405,15 +413,18 @@ namespace Avalonia.Controls _isBringIntoViewInProgress = false; _makeAnchorElement = null; + // Undo the anchor deregistrations done by OnBringIntoViewRequested. if (_scroller is object) { foreach (var child in _owner.Children) { var info = ItemsRepeater.GetVirtualizationInfo(child); - if (info.IsRealized && info.IsHeldByLayout) + // The item brought into view is still registered - don't register it more than once. + if (info.IsRealized && info.IsHeldByLayout && !info.IsRegisteredAsAnchorCandidate) { _scroller.RegisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = true; } } } @@ -430,7 +441,13 @@ namespace Avalonia.Controls { foreach (var child in _owner.Children) { - _scroller.UnregisterAnchorCandidate(child); + var info = ItemsRepeater.GetVirtualizationInfo(child); + + if (info.IsRegisteredAsAnchorCandidate) + { + _scroller.UnregisterAnchorCandidate(child); + info.IsRegisteredAsAnchorCandidate = false; + } } _scroller = null; diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index f2bd1947d6..64dfce22d4 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -361,21 +361,24 @@ namespace Avalonia.Controls Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue; } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == ValueProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == OrientationProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(change.GetNewValue()); } } diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 69922f279c..37cdefd4e5 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -1,7 +1,6 @@ using System; using System.Windows.Input; using Avalonia.Controls.Metadata; -using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; @@ -277,19 +276,21 @@ namespace Avalonia.Controls } /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (e.Property == CommandProperty) { if (_isAttachedToLogicalTree) { // Must unregister events here while a reference to the old command still exists - if (e.OldValue.GetValueOrDefault() is ICommand oldCommand) + var (oldValue, newValue) = e.GetOldAndNewValue(); + + if (oldValue is ICommand oldCommand) { oldCommand.CanExecuteChanged -= CanExecuteChanged; } - if (e.NewValue.GetValueOrDefault() is ICommand newCommand) + if (newValue is ICommand newCommand) { newCommand.CanExecuteChanged += CanExecuteChanged; } @@ -303,8 +304,7 @@ namespace Avalonia.Controls } else if (e.Property == FlyoutProperty) { - var oldFlyout = e.OldValue.GetValueOrDefault() as FlyoutBase; - var newFlyout = e.NewValue.GetValueOrDefault() as FlyoutBase; + var (oldFlyout, newFlyout) = e.GetOldAndNewValue(); // If flyout is changed while one is already open, make sure we // close the old one first @@ -316,16 +316,9 @@ namespace Avalonia.Controls } // Must unregister events here while a reference to the old flyout still exists - if (oldFlyout != null) - { - UnregisterFlyoutEvents(oldFlyout); - } - - if (newFlyout != null) - { - RegisterFlyoutEvents(newFlyout); - } + UnregisterFlyoutEvents(oldFlyout); + RegisterFlyoutEvents(newFlyout); UpdatePseudoClasses(); } @@ -417,6 +410,22 @@ namespace Avalonia.Controls } } + /// + /// Invoked when the split button's flyout is opened. + /// + protected virtual void OnFlyoutOpened() + { + // Available for derived types + } + + /// + /// Invoked when the split button's flyout is closed. + /// + protected virtual void OnFlyoutClosed() + { + // Available for derived types + } + //////////////////////////////////////////////////////////////////////// // Event Handling //////////////////////////////////////////////////////////////////////// @@ -466,6 +475,8 @@ namespace Avalonia.Controls { _isFlyoutOpen = true; UpdatePseudoClasses(); + + OnFlyoutOpened(); } } @@ -481,6 +492,8 @@ namespace Avalonia.Controls { _isFlyoutOpen = false; UpdatePseudoClasses(); + + OnFlyoutClosed(); } } } diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index 879c1aa6e1..cd34f8060a 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -90,7 +90,7 @@ namespace Avalonia.Controls //////////////////////////////////////////////////////////////////////// /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (e.Property == IsCheckedProperty) { diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index feb425a9c3..50c48d2bb0 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -123,7 +123,7 @@ namespace Avalonia.Controls index = Children.Count - 1; break; case NavigationDirection.Next: - if (index != -1) ++index; + ++index; break; case NavigationDirection.Previous: if (index != -1) --index; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 8f5aa40999..3b8842fa0e 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -76,19 +76,21 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty LineHeightProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty LineHeightProperty = + AvaloniaProperty.RegisterAttached( nameof(LineHeight), double.NaN, - validate: IsValidLineHeight); + validate: IsValidLineHeight, + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty MaxLinesProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty MaxLinesProperty = + AvaloniaProperty.RegisterAttached( nameof(MaxLines), - validate: IsValidMaxLines); + validate: IsValidMaxLines, + inherits: true); /// /// Defines the property. @@ -110,20 +112,24 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty TextAlignmentProperty = - AvaloniaProperty.Register(nameof(TextAlignment)); + public static readonly AttachedProperty TextAlignmentProperty = + AvaloniaProperty.RegisterAttached(nameof(TextAlignment), + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty TextWrappingProperty = - AvaloniaProperty.Register(nameof(TextWrapping)); + public static readonly AttachedProperty TextWrappingProperty = + AvaloniaProperty.RegisterAttached(nameof(TextWrapping), + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty TextTrimmingProperty = - AvaloniaProperty.Register(nameof(TextTrimming), defaultValue: TextTrimming.None); + public static readonly AttachedProperty TextTrimmingProperty = + AvaloniaProperty.RegisterAttached(nameof(TextTrimming), + defaultValue: TextTrimming.None, + inherits: true); /// /// Defines the property. @@ -318,6 +324,8 @@ namespace Avalonia.Controls set => SetValue(TextDecorationsProperty, value); } + protected override bool BypassFlowDirectionPolicies => true; + /// /// The BaselineOffset property provides an adjustment to baseline offset /// @@ -356,6 +364,152 @@ namespace Avalonia.Controls control.SetValue(BaselineOffsetProperty, value); } + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static TextAlignment GetTextAlignment(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextAlignmentProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextAlignment(Control control, TextAlignment alignment) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextAlignmentProperty, alignment); + } + + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static TextWrapping GetTextWrapping(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextWrappingProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextWrapping(Control control, TextWrapping wrapping) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextWrappingProperty, wrapping); + } + + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static TextTrimming GetTextTrimming(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextTrimmingProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextTrimming(Control control, TextTrimming trimming) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextTrimmingProperty, trimming); + } + + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static double GetLineHeight(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(LineHeightProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetLineHeight(Control control, double height) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(LineHeightProperty, height); + } + + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static int GetMaxLines(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(MaxLinesProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetMaxLines(Control control, int maxLines) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(MaxLinesProperty, maxLines); + } + + /// /// Renders the to a drawing context. /// @@ -485,7 +639,7 @@ namespace Avalonia.Controls private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0; - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index f8238d1da2..0be58e7fcc 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -585,7 +585,7 @@ namespace Avalonia.Controls _imClient.SetPresenter(null, null); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -594,7 +594,7 @@ namespace Avalonia.Controls UpdatePseudoclasses(); UpdateCommandStates(); } - else if (change.Property == IsUndoEnabledProperty && change.NewValue.GetValueOrDefault() == false) + else if (change.Property == IsUndoEnabledProperty && change.GetNewValue() == false) { // from docs at // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: @@ -1057,7 +1057,7 @@ namespace Avalonia.Controls SetTextInternal(editedText); - CaretIndex = end; + CaretIndex = start; } } @@ -1262,11 +1262,14 @@ namespace Avalonia.Controls return new TextBoxAutomationPeer(this); } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation( + AvaloniaProperty property, + BindingValueType state, + Exception? error) { if (property == TextProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 95a597e831..57fb82485c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -15,7 +15,6 @@ using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; -using JetBrains.Annotations; namespace Avalonia.Controls { @@ -35,8 +34,7 @@ namespace Avalonia.Controls ICloseable, IStyleHost, ILogicalRoot, - ITextInputMethodRoot, - IWeakEventSubscriber + ITextInputMethodRoot { /// /// Defines the property. @@ -87,11 +85,14 @@ namespace Avalonia.Controls private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IPlatformRenderInterface? _renderInterface; private readonly IGlobalStyles? _globalStyles; + private readonly PointerOverPreProcessor? _pointerOverPreProcessor; + private readonly IDisposable? _pointerOverPreProcessorSubscription; private Size _clientSize; private Size? _frameSize; private WindowTransparencyLevel _actualTransparencyLevel; private ILayoutManager? _layoutManager; private Border? _transparencyFallbackBorder; + private TargetWeakEventSubscriber? _resourcesChangesSubscriber; /// /// Initializes static members of the class. @@ -191,10 +192,19 @@ namespace Avalonia.Controls if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { - ResourcesChangedWeakEvent.Subscribe(applicationResources, this); + _resourcesChangesSubscriber = new TargetWeakEventSubscriber( + this, static (target, _, _, e) => + { + ((ILogical)target).NotifyResourcesChanged(e); + }); + + ResourcesChangedWeakEvent.Subscribe(applicationResources, _resourcesChangesSubscriber); } impl.LostFocus += PlatformImpl_LostFocus; + + _pointerOverPreProcessor = new PointerOverPreProcessor(this); + _pointerOverPreProcessorSubscription = _inputManager?.PreProcess.Subscribe(_pointerOverPreProcessor); } /// @@ -283,9 +293,7 @@ namespace Avalonia.Controls /// IKeyboardNavigationHandler IInputRoot.KeyboardNavigationHandler => _keyboardNavigationHandler!; - /// - /// Gets or sets the input element that the pointer is currently over. - /// + /// IInputElement? IInputRoot.PointerOverElement { get { return GetValue(PointerOverElementProperty); } @@ -295,11 +303,6 @@ namespace Avalonia.Controls /// IMouseDevice? IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; - void IWeakEventSubscriber.OnEvent(object? sender, WeakEvent ev, ResourcesChangedEventArgs e) - { - ((ILogical)this).NotifyResourcesChanged(e); - } - /// /// Gets or sets a value indicating whether access keys are shown in the window. /// @@ -350,6 +353,12 @@ namespace Avalonia.Controls /// protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this); + public override void InvalidateMirrorTransform() + { + } + + protected override bool BypassFlowDirectionPolicies => true; + /// /// Handles a paint notification from . /// @@ -372,10 +381,12 @@ namespace Avalonia.Controls Renderer?.Dispose(); Renderer = null!; - - (this as IInputRoot).MouseDevice?.TopLevelClosed(this); + + _pointerOverPreProcessor?.OnCompleted(); + _pointerOverPreProcessorSubscription?.Dispose(); + PlatformImpl = null; - + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); @@ -509,12 +520,17 @@ namespace Avalonia.Controls /// The event args. private void HandleInput(RawInputEventArgs e) { + if (e is RawPointerEventArgs pointerArgs) + { + pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position); + } + _inputManager?.ProcessInput(e); } private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) { - (this as IInputRoot).MouseDevice?.SceneInvalidated(this, e.DirtyRect); + _pointerOverPreProcessor?.SceneInvalidated(e.DirtyRect); } void PlatformImpl_LostFocus() diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs index cb0d229110..451e234653 100644 --- a/src/Avalonia.Controls/TransitioningContentControl.cs +++ b/src/Avalonia.Controls/TransitioningContentControl.cs @@ -61,7 +61,7 @@ public class TransitioningContentControl : ContentControl _lastTransitionCts?.Cancel(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index b8ab48a2b7..2ccb03e447 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -206,7 +206,7 @@ namespace Avalonia.Controls } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -216,15 +216,15 @@ namespace Avalonia.Controls } else if (change.Property == IsVisibleProperty) { - _impl?.SetIsVisible(change.NewValue.GetValueOrDefault()); + _impl?.SetIsVisible(change.GetNewValue()); } else if (change.Property == ToolTipTextProperty) { - _impl?.SetToolTipText(change.NewValue.GetValueOrDefault()); + _impl?.SetToolTipText(change.GetNewValue()); } else if (change.Property == MenuProperty) { - _impl?.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault()); + _impl?.MenuExporter?.SetNativeMenu(change.GetNewValue()); } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 1d806913dd..b2a188a2ea 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -401,7 +401,7 @@ namespace Avalonia.Controls protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() => CreateTreeItemContainerGenerator(); - protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() where TVItem: TreeViewItem, new() + protected ITreeItemContainerGenerator CreateTreeItemContainerGenerator() where TVItem: TreeViewItem, new() { return new TreeItemContainerGenerator( this, diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index a0a3c09942..490b0b3ce3 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -96,7 +96,7 @@ namespace Avalonia.Controls protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator(); /// - protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() + protected ITreeItemContainerGenerator CreateTreeItemContainerGenerator() where TVItem: TreeViewItem, new() { return new TreeItemContainerGenerator( diff --git a/src/Avalonia.Controls/Viewbox.cs b/src/Avalonia.Controls/Viewbox.cs index 50b9560cac..dd74d549bd 100644 --- a/src/Avalonia.Controls/Viewbox.cs +++ b/src/Avalonia.Controls/Viewbox.cs @@ -82,13 +82,13 @@ namespace Avalonia.Controls set => _containerVisual.RenderTransform = value; } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ChildProperty) { - _containerVisual.Child = change.NewValue.GetValueOrDefault(); + _containerVisual.Child = change.GetNewValue(); InvalidateMeasure(); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 993b3aaa1b..a5f99918b2 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -1019,16 +1019,16 @@ namespace Avalonia.Controls /// protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e); - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == SystemDecorationsProperty) { - var typedNewValue = change.NewValue.GetValueOrDefault(); + var (typedOldValue, typedNewValue) = change.GetOldAndNewValue(); PlatformImpl?.SetSystemDecorations(typedNewValue); - var o = change.OldValue.GetValueOrDefault() == SystemDecorations.Full; + var o = typedOldValue == SystemDecorations.Full; var n = typedNewValue == SystemDecorations.Full; if (o != n) diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index f8a7cdc690..0270000d8c 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -16,14 +16,8 @@ - - - - - - diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs index 6b1934ed06..f100be5d5b 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Viewport; +using Avalonia.Utilities; using InputProtocol = Avalonia.Remote.Protocol.Input; namespace Avalonia.DesignerSupport.Remote.HtmlTransport @@ -320,15 +321,13 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport ? null : modifiersText .Split(',') - .Select(x => (InputProtocol.InputModifiers)Enum.Parse( - typeof(InputProtocol.InputModifiers), x, true)) + .Select(x => EnumHelper.Parse(x, true)) .ToArray(); private static InputProtocol.MouseButton ParseMouseButton(string buttonText) => string.IsNullOrWhiteSpace(buttonText) ? InputProtocol.MouseButton.None - : (InputProtocol.MouseButton)Enum.Parse( - typeof(InputProtocol.MouseButton), buttonText, true); + : EnumHelper.Parse(buttonText, true); private static double ParseDouble(string text) => double.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json index 08ede477c3..403bb5a59a 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json @@ -377,9 +377,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index 1fc3604f70..2fb7c07b6f 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -16,14 +16,8 @@ - - - - - - @@ -34,4 +28,5 @@ + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs index cb98fb70f3..ec7e91c8be 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs @@ -92,7 +92,7 @@ namespace Avalonia.Diagnostics.Controls set => SetValue(HighlightProperty, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -102,7 +102,7 @@ namespace Avalonia.Diagnostics.Controls { _isUpdatingThickness = true; - var value = change.NewValue.GetValueOrDefault(); + var value = change.GetNewValue(); Left = value.Left; Top = value.Top; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs index 4dc0c34c0a..0c0c005122 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs @@ -110,7 +110,7 @@ namespace Avalonia.Diagnostics.ViewModels private void UpdateSizeConstraints() { - if (_control is IAvaloniaObject ao) + if (_control is AvaloniaObject ao) { string? CreateConstraintInfo(StyledProperty minProperty, StyledProperty maxProperty) { @@ -191,7 +191,7 @@ namespace Avalonia.Diagnostics.ViewModels } else { - if (_control is IAvaloniaObject ao) + if (_control is AvaloniaObject ao) { if (e.Property == Layoutable.MarginProperty) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index e08c5bc8dd..9e8a5d8d9b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -7,7 +7,6 @@ using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Metadata; using Avalonia.Threading; -using System.Reactive.Linq; using System.Linq; namespace Avalonia.Diagnostics.ViewModels @@ -59,8 +58,8 @@ namespace Avalonia.Diagnostics.ViewModels .Subscribe(e => { PointerOverRoot = e.Root; - PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault(); - }); + PointerOverElement = e.Root.InputHitTest(e.Position); + }); #nullable restore } Console = new ConsoleViewModel(UpdateConsoleContext); @@ -163,8 +162,7 @@ namespace Avalonia.Diagnostics.ViewModels } catch { } }, - TimeSpan.FromMilliseconds(0), - DispatcherPriority.ApplicationIdle); + TimeSpan.FromMilliseconds(0)); } RaiseAndSetIfChanged(ref _content, value); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 2553ae90ce..58807b489e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -36,6 +36,7 @@ namespace Avalonia.Diagnostics.Views new Border { BorderBrush = new SolidColorBrush(Colors.Yellow, 0.5) } }, }; + AdornerLayer.SetIsClipEnabled(_adorner, false); } protected void AddAdorner(object? sender, PointerEventArgs e) diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index 770d15ea27..a311efdfb0 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -15,4 +15,5 @@ + diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 90221bb922..addc248d58 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -7,7 +7,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; +using Avalonia.Media.Imaging; namespace Avalonia.Headless { diff --git a/src/Avalonia.Input/ApiCompatBaseline.txt b/src/Avalonia.Input/ApiCompatBaseline.txt deleted file mode 100644 index 68b4d4754f..0000000000 --- a/src/Avalonia.Input/ApiCompatBaseline.txt +++ /dev/null @@ -1,29 +0,0 @@ -Compat issues with assembly Avalonia.Input: -MembersMustExist : Member 'public Avalonia.Platform.IPlatformHandle Avalonia.Input.Cursor.PlatformCursor.get()' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.RightTappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.TappedEvent' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.IFocusManager.RemoveFocusScope(Avalonia.Input.IFocusScope)' is present in the implementation but not in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.InputElement.TextInputOptionsQueryEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.InputElement.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.InputElement.TappedEvent' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_Tapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_TextInputOptionsQuery(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_Tapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_TextInputOptionsQuery(System.EventHandler)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetActive(System.Boolean)' is present in the contract but not in the implementation. -MembersMustExist : Member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetActive(System.Boolean)' does not exist in the implementation but it does exist in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetClient(Avalonia.Input.TextInput.ITextInputMethodClient)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetOptions(Avalonia.Input.TextInput.TextInputOptions)' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetOptions(Avalonia.Input.TextInput.TextInputOptionsQueryEventArgs)' is present in the contract but not in the implementation. -MembersMustExist : Member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetOptions(Avalonia.Input.TextInput.TextInputOptionsQueryEventArgs)' does not exist in the implementation but it does exist in the contract. -EnumValuesMustMatch : Enum value 'Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType.Email' is (System.Int32)5 in the implementation but (System.Int32)1 in the contract. -EnumValuesMustMatch : Enum value 'Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType.Number' is (System.Int32)4 in the implementation but (System.Int32)3 in the contract. -EnumValuesMustMatch : Enum value 'Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType.Password' is (System.Int32)8 in the implementation but (System.Int32)5 in the contract. -MembersMustExist : Member 'public Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType.Phone' does not exist in the implementation but it does exist in the contract. -EnumValuesMustMatch : Enum value 'Avalonia.Input.TextInput.TextInputContentType Avalonia.Input.TextInput.TextInputContentType.Url' is (System.Int32)6 in the implementation but (System.Int32)4 in the contract. -TypesMustExist : Type 'Avalonia.Input.TextInput.TextInputOptionsQueryEventArgs' does not exist in the implementation but it does exist in the contract. -TypesMustExist : Type 'Avalonia.Platform.IStandardCursorFactory' does not exist in the implementation but it does exist in the contract. -Total Issues: 27 diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj deleted file mode 100644 index e66d1d7e7c..0000000000 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - net6.0;netstandard2.0 - - - - - - - - - - - - - - - diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs deleted file mode 100644 index 86e6e96a71..0000000000 --- a/src/Avalonia.Input/Gestures.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public static class Gestures - { - private static bool s_isDoubleTapped = false; - public static readonly RoutedEvent TappedEvent = RoutedEvent.Register( - "Tapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent DoubleTappedEvent = RoutedEvent.Register( - "DoubleTapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent RightTappedEvent = RoutedEvent.Register( - "RightTapped", - RoutingStrategies.Bubble, - typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureEvent = - RoutedEvent.Register( - "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent ScrollGestureEndedEvent = - RoutedEvent.Register( - "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureMagnifyEvent = - RoutedEvent.Register( - "PointerMagnifyGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureRotateEvent = - RoutedEvent.Register( - "PointerRotateGesture", RoutingStrategies.Bubble, typeof(Gestures)); - - public static readonly RoutedEvent PointerTouchPadGestureSwipeEvent = - RoutedEvent.Register( - "PointerSwipeGesture", RoutingStrategies.Bubble, typeof(Gestures)); - -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - private static readonly WeakReference s_lastPress = new WeakReference(null); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - - static Gestures() - { - InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); - InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); - } - - public static void AddTappedHandler(IInteractive element, EventHandler handler) - { - element.AddHandler(TappedEvent, handler); - } - - public static void AddDoubleTappedHandler(IInteractive element, EventHandler handler) - { - element.AddHandler(DoubleTappedEvent, handler); - } - - public static void AddRightTappedHandler(IInteractive element, EventHandler handler) - { - element.AddHandler(RightTappedEvent, handler); - } - - public static void RemoveTappedHandler(IInteractive element, EventHandler handler) - { - element.RemoveHandler(TappedEvent, handler); - } - - public static void RemoveDoubleTappedHandler(IInteractive element, EventHandler handler) - { - element.RemoveHandler(DoubleTappedEvent, handler); - } - - public static void RemoveRightTappedHandler(IInteractive element, EventHandler handler) - { - element.RemoveHandler(RightTappedEvent, handler); - } - - private static void PointerPressed(RoutedEventArgs ev) - { - if (ev.Source is null) - { - return; - } - - if (ev.Route == RoutingStrategies.Bubble) - { - var e = (PointerPressedEventArgs)ev; - var visual = (IVisual)ev.Source; - - if (e.ClickCount <= 1) - { - s_isDoubleTapped = false; - s_lastPress.SetTarget(ev.Source); - } - else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) - { - if (s_lastPress.TryGetTarget(out var target) && target == e.Source) - { - s_isDoubleTapped = true; - e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); - } - } - else - { - s_isDoubleTapped = false; - } - } - } - - private static void PointerReleased(RoutedEventArgs ev) - { - if (ev.Route == RoutingStrategies.Bubble) - { - var e = (PointerReleasedEventArgs)ev; - - if (s_lastPress.TryGetTarget(out var target) && target == e.Source) - { - if (e.InitialPressMouseButton == MouseButton.Left || e.InitialPressMouseButton == MouseButton.Right) - { - if (e.InitialPressMouseButton == MouseButton.Right) - { - e.Source.RaiseEvent(new TappedEventArgs(RightTappedEvent, e)); - } - //s_isDoubleTapped needed here to prevent invoking Tapped event when DoubleTapped is called. - //This behaviour matches UWP behaviour. - else if (s_isDoubleTapped == false) - { - e.Source.RaiseEvent(new TappedEventArgs(TappedEvent, e)); - } - } - } - } - } - } -} diff --git a/src/Avalonia.Input/IInputRoot.cs b/src/Avalonia.Input/IInputRoot.cs deleted file mode 100644 index 3e2b8cc477..0000000000 --- a/src/Avalonia.Input/IInputRoot.cs +++ /dev/null @@ -1,36 +0,0 @@ -using JetBrains.Annotations; - -namespace Avalonia.Input -{ - /// - /// Defines the interface for top-level input elements. - /// - public interface IInputRoot : IInputElement - { - /// - /// Gets or sets the access key handler. - /// - IAccessKeyHandler AccessKeyHandler { get; } - - /// - /// Gets or sets the keyboard navigation handler. - /// - IKeyboardNavigationHandler KeyboardNavigationHandler { get; } - - /// - /// Gets or sets the input element that the pointer is currently over. - /// - IInputElement? PointerOverElement { get; set; } - - /// - /// Gets or sets a value indicating whether access keys are shown in the window. - /// - bool ShowAccessKeys { get; set; } - - /// - /// Gets associated mouse device - /// - [CanBeNull] - IMouseDevice? MouseDevice { get; } - } -} diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs deleted file mode 100644 index 9506dc36fb..0000000000 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.ComponentModel; - -namespace Avalonia.Input -{ - [Flags, Obsolete("Use KeyModifiers and PointerPointProperties")] - public enum InputModifiers - { - None = 0, - Alt = 1, - Control = 2, - Shift = 4, - Windows = 8, - LeftMouseButton = 16, - RightMouseButton = 32, - MiddleMouseButton = 64 - } - - [Flags] - public enum KeyModifiers - { - None = 0, - Alt = 1, - Control = 2, - Shift = 4, - Meta = 8, - } - - [Flags] - public enum KeyStates - { - None = 0, - Down = 1, - Toggled = 2, - } - - [Flags] - public enum RawInputModifiers - { - None = 0, - Alt = 1, - Control = 2, - Shift = 4, - Meta = 8, - LeftMouseButton = 16, - RightMouseButton = 32, - MiddleMouseButton = 64, - XButton1MouseButton = 128, - XButton2MouseButton = 256, - KeyboardMask = Alt | Control | Shift | Meta - } - - internal static class KeyModifiersUtils - { - public static KeyModifiers ConvertToKey(RawInputModifiers modifiers) => - (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); - } - - public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged - { - IInputElement? FocusedElement { get; } - - void SetFocusedElement( - IInputElement? element, - NavigationMethod method, - KeyModifiers modifiers); - } -} diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs deleted file mode 100644 index 272d1eb8d7..0000000000 --- a/src/Avalonia.Input/IMouseDevice.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Avalonia.Input -{ - /// - /// Represents a mouse device. - /// - public interface IMouseDevice : IPointerDevice - { - /// - /// Gets the mouse position, in screen coordinates. - /// - [Obsolete("Use PointerEventArgs.GetPosition")] - PixelPoint Position { get; } - - void TopLevelClosed(IInputRoot root); - - void SceneInvalidated(IInputRoot root, Rect rect); - } -} diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs deleted file mode 100644 index 1f82cb1ed7..0000000000 --- a/src/Avalonia.Input/IPointerDevice.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public interface IPointerDevice : IInputDevice - { - [Obsolete("Use IPointer")] - IInputElement? Captured { get; } - - [Obsolete("Use IPointer")] - void Capture(IInputElement? control); - - [Obsolete("Use PointerEventArgs.GetPosition")] - Point GetPosition(IVisual relativeTo); - } -} diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs deleted file mode 100644 index 6bc9294ddd..0000000000 --- a/src/Avalonia.Input/InputElement.cs +++ /dev/null @@ -1,693 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Metadata; -using Avalonia.Data; -using Avalonia.Input.GestureRecognizers; -using Avalonia.Input.TextInput; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -#nullable enable - -namespace Avalonia.Input -{ - /// - /// Implements input-related functionality for a control. - /// - [PseudoClasses(":disabled", ":focus", ":focus-visible", ":focus-within", ":pointerover")] - public class InputElement : Interactive, IInputElement - { - /// - /// Defines the property. - /// - public static readonly StyledProperty FocusableProperty = - AvaloniaProperty.Register(nameof(Focusable)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsEnabledProperty = - AvaloniaProperty.Register(nameof(IsEnabled), true); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsEffectivelyEnabledProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsEffectivelyEnabled), - o => o.IsEffectivelyEnabled); - - /// - /// Gets or sets associated mouse cursor. - /// - public static readonly StyledProperty CursorProperty = - AvaloniaProperty.Register(nameof(Cursor), null, true); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsKeyboardFocusWithinProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsKeyboardFocusWithin), - o => o.IsKeyboardFocusWithin); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsFocusedProperty = - AvaloniaProperty.RegisterDirect(nameof(IsFocused), o => o.IsFocused); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsHitTestVisibleProperty = - AvaloniaProperty.Register(nameof(IsHitTestVisible), true); - - /// - /// Defines the property. - /// - public static readonly DirectProperty IsPointerOverProperty = - AvaloniaProperty.RegisterDirect(nameof(IsPointerOver), o => o.IsPointerOver); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsTabStopProperty = - KeyboardNavigation.IsTabStopProperty.AddOwner(); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent GotFocusEvent = - RoutedEvent.Register(nameof(GotFocus), RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent LostFocusEvent = - RoutedEvent.Register(nameof(LostFocus), RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent KeyDownEvent = - RoutedEvent.Register( - "KeyDown", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent KeyUpEvent = - RoutedEvent.Register( - "KeyUp", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the property. - /// - public static readonly StyledProperty TabIndexProperty = - KeyboardNavigation.TabIndexProperty.AddOwner(); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent TextInputEvent = - RoutedEvent.Register( - "TextInput", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent TextInputMethodClientRequestedEvent = - RoutedEvent.Register( - "TextInputMethodClientRequested", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerEnterEvent = - RoutedEvent.Register(nameof(PointerEnter), RoutingStrategies.Direct); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerLeaveEvent = - RoutedEvent.Register(nameof(PointerLeave), RoutingStrategies.Direct); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerMovedEvent = - RoutedEvent.Register( - "PointerMove", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerPressedEvent = - RoutedEvent.Register( - "PointerPressed", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerReleasedEvent = - RoutedEvent.Register( - "PointerReleased", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the routed event. - /// - public static readonly RoutedEvent PointerCaptureLostEvent = - RoutedEvent.Register( - "PointerCaptureLost", - RoutingStrategies.Direct); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent PointerWheelChangedEvent = - RoutedEvent.Register( - "PointerWheelChanged", - RoutingStrategies.Tunnel | RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; - - /// - /// Defines the event. - /// - public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; - - private bool _isEffectivelyEnabled = true; - private bool _isFocused; - private bool _isKeyboardFocusWithin; - private bool _isFocusVisible; - private bool _isPointerOver; - private GestureRecognizerCollection? _gestureRecognizers; - - /// - /// Initializes static members of the class. - /// - static InputElement() - { - IsEnabledProperty.Changed.Subscribe(IsEnabledChanged); - - GotFocusEvent.AddClassHandler((x, e) => x.OnGotFocus(e)); - LostFocusEvent.AddClassHandler((x, e) => x.OnLostFocus(e)); - KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e)); - KeyUpEvent.AddClassHandler((x, e) => x.OnKeyUp(e)); - TextInputEvent.AddClassHandler((x, e) => x.OnTextInput(e)); - PointerEnterEvent.AddClassHandler((x, e) => x.OnPointerEnterCore(e)); - PointerLeaveEvent.AddClassHandler((x, e) => x.OnPointerLeaveCore(e)); - PointerMovedEvent.AddClassHandler((x, e) => x.OnPointerMoved(e)); - PointerPressedEvent.AddClassHandler((x, e) => x.OnPointerPressed(e)); - PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); - PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); - PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); - } - - public InputElement() - { - UpdatePseudoClasses(IsFocused, IsPointerOver); - } - - /// - /// Occurs when the control receives focus. - /// - public event EventHandler? GotFocus - { - add { AddHandler(GotFocusEvent, value); } - remove { RemoveHandler(GotFocusEvent, value); } - } - - /// - /// Occurs when the control loses focus. - /// - public event EventHandler? LostFocus - { - add { AddHandler(LostFocusEvent, value); } - remove { RemoveHandler(LostFocusEvent, value); } - } - - /// - /// Occurs when a key is pressed while the control has focus. - /// - public event EventHandler? KeyDown - { - add { AddHandler(KeyDownEvent, value); } - remove { RemoveHandler(KeyDownEvent, value); } - } - - /// - /// Occurs when a key is released while the control has focus. - /// - public event EventHandler? KeyUp - { - add { AddHandler(KeyUpEvent, value); } - remove { RemoveHandler(KeyUpEvent, value); } - } - - /// - /// Occurs when a user typed some text while the control has focus. - /// - public event EventHandler? TextInput - { - add { AddHandler(TextInputEvent, value); } - remove { RemoveHandler(TextInputEvent, value); } - } - - /// - /// Occurs when an input element gains input focus and input method is looking for the corresponding client - /// - public event EventHandler? TextInputMethodClientRequested - { - add { AddHandler(TextInputMethodClientRequestedEvent, value); } - remove { RemoveHandler(TextInputMethodClientRequestedEvent, value); } - } - - /// - /// Occurs when the pointer enters the control. - /// - public event EventHandler? PointerEnter - { - add { AddHandler(PointerEnterEvent, value); } - remove { RemoveHandler(PointerEnterEvent, value); } - } - - /// - /// Occurs when the pointer leaves the control. - /// - public event EventHandler? PointerLeave - { - add { AddHandler(PointerLeaveEvent, value); } - remove { RemoveHandler(PointerLeaveEvent, value); } - } - - /// - /// Occurs when the pointer moves over the control. - /// - public event EventHandler? PointerMoved - { - add { AddHandler(PointerMovedEvent, value); } - remove { RemoveHandler(PointerMovedEvent, value); } - } - - /// - /// Occurs when the pointer is pressed over the control. - /// - public event EventHandler? PointerPressed - { - add { AddHandler(PointerPressedEvent, value); } - remove { RemoveHandler(PointerPressedEvent, value); } - } - - /// - /// Occurs when the pointer is released over the control. - /// - public event EventHandler? PointerReleased - { - add { AddHandler(PointerReleasedEvent, value); } - remove { RemoveHandler(PointerReleasedEvent, value); } - } - - /// - /// Occurs when the control or its child control loses the pointer capture for any reason, - /// event will not be triggered for a parent control if capture was transferred to another child of that parent control - /// - public event EventHandler? PointerCaptureLost - { - add => AddHandler(PointerCaptureLostEvent, value); - remove => RemoveHandler(PointerCaptureLostEvent, value); - } - - /// - /// Occurs when the mouse is scrolled over the control. - /// - public event EventHandler? PointerWheelChanged - { - add { AddHandler(PointerWheelChangedEvent, value); } - remove { RemoveHandler(PointerWheelChangedEvent, value); } - } - - /// - /// Occurs when a tap gesture occurs on the control. - /// - public event EventHandler? Tapped - { - add { AddHandler(TappedEvent, value); } - remove { RemoveHandler(TappedEvent, value); } - } - - /// - /// Occurs when a double-tap gesture occurs on the control. - /// - public event EventHandler? DoubleTapped - { - add { AddHandler(DoubleTappedEvent, value); } - remove { RemoveHandler(DoubleTappedEvent, value); } - } - - /// - /// Gets or sets a value indicating whether the control can receive focus. - /// - public bool Focusable - { - get { return GetValue(FocusableProperty); } - set { SetValue(FocusableProperty, value); } - } - - /// - /// Gets or sets a value indicating whether the control is enabled for user interaction. - /// - public bool IsEnabled - { - get { return GetValue(IsEnabledProperty); } - set { SetValue(IsEnabledProperty, value); } - } - - /// - /// Gets or sets associated mouse cursor. - /// - public Cursor? Cursor - { - get { return GetValue(CursorProperty); } - set { SetValue(CursorProperty, value); } - } - - /// - /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements. - /// - public bool IsKeyboardFocusWithin - { - get => _isKeyboardFocusWithin; - internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value); - } - - /// - /// Gets a value indicating whether the control is focused. - /// - public bool IsFocused - { - get { return _isFocused; } - private set { SetAndRaise(IsFocusedProperty, ref _isFocused, value); } - } - - /// - /// Gets or sets a value indicating whether the control is considered for hit testing. - /// - public bool IsHitTestVisible - { - get { return GetValue(IsHitTestVisibleProperty); } - set { SetValue(IsHitTestVisibleProperty, value); } - } - - /// - /// Gets a value indicating whether the pointer is currently over the control. - /// - public bool IsPointerOver - { - get { return _isPointerOver; } - internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } - } - - /// - /// Gets or sets a value that indicates whether the control is included in tab navigation. - /// - public bool IsTabStop - { - get => GetValue(IsTabStopProperty); - set => SetValue(IsTabStopProperty, value); - } - - /// - public bool IsEffectivelyEnabled - { - get => _isEffectivelyEnabled; - private set - { - SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); - PseudoClasses.Set(":disabled", !value); - } - } - - /// - /// Gets or sets a value that determines the order in which elements receive focus when the - /// user navigates through controls by pressing the Tab key. - /// - public int TabIndex - { - get => GetValue(TabIndexProperty); - set => SetValue(TabIndexProperty, value); - } - - public List KeyBindings { get; } = new List(); - - /// - /// Allows a derived class to override the enabled state of the control. - /// - /// - /// Derived controls may wish to disable the enabled state of the control without overwriting the - /// user-supplied setting. This can be done by overriding this property - /// to return the overridden enabled state. If the value returned from - /// should change, then the derived control should call . - /// - protected virtual bool IsEnabledCore => IsEnabled; - - public GestureRecognizerCollection GestureRecognizers - => _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this)); - - /// - /// Focuses the control. - /// - public void Focus() - { - FocusManager.Instance?.Focus(this); - } - - /// - protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTreeCore(e); - - if (IsFocused) - { - FocusManager.Instance?.Focus(null); - } - } - - /// - protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTreeCore(e); - UpdateIsEffectivelyEnabled(); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnGotFocus(GotFocusEventArgs e) - { - var isFocused = e.Source == this; - _isFocusVisible = isFocused && (e.NavigationMethod == NavigationMethod.Directional || e.NavigationMethod == NavigationMethod.Tab); - IsFocused = isFocused; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnLostFocus(RoutedEventArgs e) - { - _isFocusVisible = false; - IsFocused = false; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnKeyDown(KeyEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnKeyUp(KeyEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnTextInput(TextInputEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerEnter(PointerEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerLeave(PointerEventArgs e) - { - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerMoved(PointerEventArgs e) - { - if (_gestureRecognizers?.HandlePointerMoved(e) == true) - e.Handled = true; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerPressed(PointerPressedEventArgs e) - { - if (_gestureRecognizers?.HandlePointerPressed(e) == true) - e.Handled = true; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerReleased(PointerReleasedEventArgs e) - { - if (_gestureRecognizers?.HandlePointerReleased(e) == true) - e.Handled = true; - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e) - { - _gestureRecognizers?.HandlePointerCaptureLost(e); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - protected virtual void OnPointerWheelChanged(PointerWheelEventArgs e) - { - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == IsFocusedProperty) - { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault(), null); - } - else if (change.Property == IsPointerOverProperty) - { - UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); - } - else if (change.Property == IsKeyboardFocusWithinProperty) - { - PseudoClasses.Set(":focus-within", change.NewValue.GetValueOrDefault()); - } - } - - /// - /// Updates the property value according to the parent - /// control's enabled state and . - /// - protected void UpdateIsEffectivelyEnabled() - { - UpdateIsEffectivelyEnabled(this.GetVisualParent()); - } - - private static void IsEnabledChanged(AvaloniaPropertyChangedEventArgs e) - { - ((InputElement)e.Sender).UpdateIsEffectivelyEnabled(); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - private void OnPointerEnterCore(PointerEventArgs e) - { - IsPointerOver = true; - OnPointerEnter(e); - } - - /// - /// Called before the event occurs. - /// - /// The event args. - private void OnPointerLeaveCore(PointerEventArgs e) - { - IsPointerOver = false; - OnPointerLeave(e); - } - - /// - /// Updates the property based on the parent's - /// . - /// - /// The parent control. - private void UpdateIsEffectivelyEnabled(InputElement? parent) - { - IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true); - - // PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ - // will cause extra allocations and overhead. - - var children = VisualChildren; - - // ReSharper disable once ForCanBeConvertedToForeach - for (int i = 0; i < children.Count; ++i) - { - var child = children[i] as InputElement; - - child?.UpdateIsEffectivelyEnabled(this); - } - } - - private void UpdatePseudoClasses(bool? isFocused, bool? isPointerOver) - { - if (isFocused.HasValue) - { - PseudoClasses.Set(":focus", isFocused.Value); - PseudoClasses.Set(":focus-visible", _isFocusVisible); - } - - if (isPointerOver.HasValue) - { - PseudoClasses.Set(":pointerover", isPointerOver.Value); - } - } - } -} diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs deleted file mode 100644 index 0adbe73263..0000000000 --- a/src/Avalonia.Input/KeyGesture.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Input -{ - /// - /// Defines a keyboard input combination. - /// - public sealed class KeyGesture : IEquatable - { - private static readonly Dictionary s_keySynonyms = new Dictionary - { - { "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod }, { ",", Key.OemComma } - }; - - [Obsolete("Use constructor taking KeyModifiers")] - public KeyGesture(Key key, InputModifiers modifiers) - { - Key = key; - KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xf); - } - - public KeyGesture(Key key, KeyModifiers modifiers = KeyModifiers.None) - { - Key = key; - KeyModifiers = modifiers; - } - - public bool Equals(KeyGesture? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - - return Key == other.Key && KeyModifiers == other.KeyModifiers; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - - return obj is KeyGesture gesture && Equals(gesture); - } - - public override int GetHashCode() - { - unchecked - { - return ((int)Key * 397) ^ (int)KeyModifiers; - } - } - - public static bool operator ==(KeyGesture? left, KeyGesture? right) - { - return Equals(left, right); - } - - public static bool operator !=(KeyGesture? left, KeyGesture? right) - { - return !Equals(left, right); - } - - public Key Key { get; } - - [Obsolete("Use KeyModifiers")] - public InputModifiers Modifiers => (InputModifiers)KeyModifiers; - - public KeyModifiers KeyModifiers { get; } - - public static KeyGesture Parse(string gesture) - { - // string.Split can't be used here because "Ctrl++" is a perfectly valid key gesture - - var key = Key.None; - var keyModifiers = KeyModifiers.None; - - var cstart = 0; - - for (var c = 0; c <= gesture.Length; c++) - { - var ch = c == gesture.Length ? '\0' : gesture[c]; - bool isLast = c == gesture.Length; - - if (isLast || (ch == '+' && cstart != c)) - { - var partSpan = gesture.AsSpan(cstart, c - cstart).Trim(); - - if (isLast) - { - key = ParseKey(partSpan.ToString()); - } - else - { - keyModifiers |= ParseModifier(partSpan); - } - - cstart = c + 1; - } - } - - - return new KeyGesture(key, keyModifiers); - } - - public override string ToString() - { - var s = new StringBuilder(); - - static void Plus(StringBuilder s) - { - if (s.Length > 0) - { - s.Append("+"); - } - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Control)) - { - s.Append("Ctrl"); - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Shift)) - { - Plus(s); - s.Append("Shift"); - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Alt)) - { - Plus(s); - s.Append("Alt"); - } - - if (KeyModifiers.HasAllFlags(KeyModifiers.Meta)) - { - Plus(s); - s.Append("Cmd"); - } - - Plus(s); - s.Append(Key); - - return s.ToString(); - } - - public bool Matches(KeyEventArgs keyEvent) => - keyEvent != null && - keyEvent.KeyModifiers == KeyModifiers && - ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key); - - // TODO: Move that to external key parser - private static Key ParseKey(string key) - { - if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv)) - return rv; - - return (Key)Enum.Parse(typeof(Key), key, true); - } - - private static KeyModifiers ParseModifier(ReadOnlySpan modifier) - { - if (modifier.Equals("ctrl".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - return KeyModifiers.Control; - } - - if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase) || - modifier.Equals("win".AsSpan(), StringComparison.OrdinalIgnoreCase) || - modifier.Equals("⌘".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - return KeyModifiers.Meta; - } - - return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true); - } - - private Key ResolveNumPadOperationKey(Key key) - { - switch (key) - { - case Key.Add: - return Key.OemPlus; - case Key.Subtract: - return Key.OemMinus; - case Key.Decimal: - return Key.OemPeriod; - default: - return key; - } - } - } -} diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs deleted file mode 100644 index 3df717b8c4..0000000000 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System.ComponentModel; -using System.Runtime.CompilerServices; -using Avalonia.Input.Raw; -using Avalonia.Input.TextInput; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged - { - private IInputElement? _focusedElement; - private IInputRoot? _focusedRoot; - - public event PropertyChangedEventHandler? PropertyChanged; - - public static IKeyboardDevice? Instance => AvaloniaLocator.Current.GetService(); - - public IInputManager? InputManager => AvaloniaLocator.Current.GetService(); - - public IFocusManager? FocusManager => AvaloniaLocator.Current.GetService(); - - // This should live in the FocusManager, but with the current outdated architecture - // the source of truth about the input focus is in KeyboardDevice - private readonly TextInputMethodManager _textInputManager = new TextInputMethodManager(); - - public IInputElement? FocusedElement => _focusedElement; - - private void ClearFocusWithinAncestors(IInputElement? element) - { - var el = element; - - while (el != null) - { - if (el is InputElement ie) - { - ie.IsKeyboardFocusWithin = false; - } - - el = (IInputElement?)el.VisualParent; - } - } - - private void ClearFocusWithin(IInputElement element, bool clearRoot) - { - foreach (var visual in element.VisualChildren) - { - if (visual is IInputElement el && el.IsKeyboardFocusWithin) - { - ClearFocusWithin(el, true); - break; - } - } - - if (clearRoot) - { - if (element is InputElement ie) - { - ie.IsKeyboardFocusWithin = false; - } - } - } - - private void SetIsFocusWithin(IInputElement? oldElement, IInputElement? newElement) - { - if (newElement == null && oldElement != null) - { - ClearFocusWithinAncestors(oldElement); - return; - } - - IInputElement? branch = null; - - var el = newElement; - - while (el != null) - { - if (el.IsKeyboardFocusWithin) - { - branch = el; - break; - } - - el = el.VisualParent as IInputElement; - } - - el = oldElement; - - if (el != null && branch != null) - { - ClearFocusWithin(branch, false); - } - - el = newElement; - - while (el != null && el != branch) - { - if (el is InputElement ie) - { - ie.IsKeyboardFocusWithin = true; - } - - el = el.VisualParent as IInputElement; - } - } - - private void ClearChildrenFocusWithin(IInputElement element, bool clearRoot) - { - foreach (var visual in element.VisualChildren) - { - if (visual is IInputElement el && el.IsKeyboardFocusWithin) - { - ClearChildrenFocusWithin(el, true); - break; - } - } - - if (clearRoot && element is InputElement ie) - { - ie.IsKeyboardFocusWithin = false; - } - } - - public void SetFocusedElement( - IInputElement? element, - NavigationMethod method, - KeyModifiers keyModifiers) - { - if (element != FocusedElement) - { - var interactive = FocusedElement as IInteractive; - - if (FocusedElement != null && - (!FocusedElement.IsAttachedToVisualTree || - _focusedRoot != element?.VisualRoot as IInputRoot) && - _focusedRoot != null) - { - ClearChildrenFocusWithin(_focusedRoot, true); - } - - SetIsFocusWithin(FocusedElement, element); - _focusedElement = element; - _focusedRoot = _focusedElement?.VisualRoot as IInputRoot; - - interactive?.RaiseEvent(new RoutedEventArgs - { - RoutedEvent = InputElement.LostFocusEvent, - }); - - interactive = element as IInteractive; - - interactive?.RaiseEvent(new GotFocusEventArgs - { - RoutedEvent = InputElement.GotFocusEvent, - NavigationMethod = method, - KeyModifiers = keyModifiers, - }); - - _textInputManager.SetFocusedElement(element); - RaisePropertyChanged(nameof(FocusedElement)); - } - } - - protected void RaisePropertyChanged([CallerMemberName] string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - public void ProcessRawEvent(RawInputEventArgs e) - { - if(e.Handled) - return; - - var element = FocusedElement ?? e.Root; - - if (e is RawKeyEventArgs keyInput) - { - switch (keyInput.Type) - { - case RawKeyEventType.KeyDown: - case RawKeyEventType.KeyUp: - var routedEvent = keyInput.Type == RawKeyEventType.KeyDown - ? InputElement.KeyDownEvent - : InputElement.KeyUpEvent; - - KeyEventArgs ev = new KeyEventArgs - { - RoutedEvent = routedEvent, - Device = this, - Key = keyInput.Key, - KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), - Source = element, - }; - - IVisual? currentHandler = element; - while (currentHandler != null && !ev.Handled && keyInput.Type == RawKeyEventType.KeyDown) - { - var bindings = (currentHandler as IInputElement)?.KeyBindings; - if (bindings != null) - { - KeyBinding[]? bindingsCopy = null; - - // Create a copy of the KeyBindings list if there's a binding which matches the event. - // If we don't do this the foreach loop will throw an InvalidOperationException when the KeyBindings list is changed. - // This can happen when a new view is loaded which adds its own KeyBindings to the handler. - foreach (var binding in bindings) - { - if (binding.Gesture?.Matches(ev) == true) - { - bindingsCopy = bindings.ToArray(); - break; - } - } - - if (bindingsCopy is object) - { - foreach (var binding in bindingsCopy) - { - if (ev.Handled) - break; - binding.TryHandle(ev); - } - } - } - currentHandler = currentHandler.VisualParent; - } - - element.RaiseEvent(ev); - e.Handled = ev.Handled; - break; - } - } - - if (e is RawTextInputEventArgs text) - { - var ev = new TextInputEventArgs() - { - Device = this, - Text = text.Text, - Source = element, - RoutedEvent = InputElement.TextInputEvent - }; - - element.RaiseEvent(ev); - e.Handled = ev.Handled; - } - } - } -} diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs deleted file mode 100644 index a25aed6811..0000000000 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ /dev/null @@ -1,134 +0,0 @@ -namespace Avalonia.Input -{ - /// - /// Defines attached properties that control keyboard navigation behaviour for a container. - /// - public static class KeyboardNavigation - { - /// - /// Defines the TabIndex attached property. - /// - public static readonly AttachedProperty TabIndexProperty = - AvaloniaProperty.RegisterAttached( - "TabIndex", - typeof(KeyboardNavigation), - int.MaxValue); - - /// - /// Defines the TabNavigation attached property. - /// - /// - /// The TabNavigation attached property defines how pressing the Tab key causes focus to - /// be navigated between the children of the container. - /// - public static readonly AttachedProperty TabNavigationProperty = - AvaloniaProperty.RegisterAttached( - "TabNavigation", - typeof(KeyboardNavigation)); - - /// - /// Defines the TabOnceActiveElement attached property. - /// - /// - /// When focus enters a container which has its - /// attached property set to , this property - /// defines to which child the focus should move. - /// - public static readonly AttachedProperty TabOnceActiveElementProperty = - AvaloniaProperty.RegisterAttached( - "TabOnceActiveElement", - typeof(KeyboardNavigation)); - - /// - /// Defines the IsTabStop attached property. - /// - /// - /// The IsTabStop attached property determines whether the control is focusable by tab navigation. - /// - public static readonly AttachedProperty IsTabStopProperty = - AvaloniaProperty.RegisterAttached( - "IsTabStop", - typeof(KeyboardNavigation), - true); - - /// - /// Gets the for an element. - /// - /// The container. - /// The for the container. - public static int GetTabIndex(IInputElement element) - { - return ((IAvaloniaObject)element).GetValue(TabIndexProperty); - } - - /// - /// Sets the for an element. - /// - /// The element. - /// The tab index. - public static void SetTabIndex(IInputElement element, int value) - { - ((IAvaloniaObject)element).SetValue(TabIndexProperty, value); - } - - /// - /// Gets the for a container. - /// - /// The container. - /// The for the container. - public static KeyboardNavigationMode GetTabNavigation(InputElement element) - { - return element.GetValue(TabNavigationProperty); - } - - /// - /// Sets the for a container. - /// - /// The container. - /// The for the container. - public static void SetTabNavigation(InputElement element, KeyboardNavigationMode value) - { - element.SetValue(TabNavigationProperty, value); - } - - /// - /// Gets the for a container. - /// - /// The container. - /// The active element for the container. - public static IInputElement? GetTabOnceActiveElement(InputElement element) - { - return element.GetValue(TabOnceActiveElementProperty); - } - - /// - /// Sets the for a container. - /// - /// The container. - /// The active element for the container. - public static void SetTabOnceActiveElement(InputElement element, IInputElement? value) - { - element.SetValue(TabOnceActiveElementProperty, value); - } - - /// - /// Sets the for an element. - /// - /// The container. - /// Value indicating whether the container is a tab stop. - public static void SetIsTabStop(InputElement element, bool value) - { - element.SetValue(IsTabStopProperty, value); - } - - /// - /// Gets the for an element. - /// - /// The container. - /// Whether the container is a tab stop. - public static bool GetIsTabStop(InputElement element) - { - return element.GetValue(IsTabStopProperty); - } - } -} diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs deleted file mode 100644 index a5d54bb047..0000000000 --- a/src/Avalonia.Input/MouseDevice.cs +++ /dev/null @@ -1,571 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Input.Raw; -using Avalonia.Interactivity; -using Avalonia.Platform; -using Avalonia.Utilities; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - /// - /// Represents a mouse device. - /// - public class MouseDevice : IMouseDevice, IDisposable - { - private int _clickCount; - private Rect _lastClickRect; - private ulong _lastClickTime; - - private readonly Pointer _pointer; - private bool _disposed; - private PixelPoint? _position; - - public MouseDevice(Pointer? pointer = null) - { - _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); - } - - /// - /// Gets the control that is currently capturing by the mouse, if any. - /// - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. To set the mouse capture, call the - /// method. - /// - [Obsolete("Use IPointer instead")] - public IInputElement? Captured => _pointer.Captured; - - /// - /// Gets the mouse position, in screen coordinates. - /// - [Obsolete("Use events instead")] - public PixelPoint Position - { - get => _position ?? new PixelPoint(-1, -1); - protected set => _position = value; - } - - /// - /// Captures mouse input to the specified control. - /// - /// The control. - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. The current mouse capture control is exposed - /// by the property. - /// - public void Capture(IInputElement? control) - { - _pointer.Capture(control); - } - - /// - /// Gets the mouse position relative to a control. - /// - /// The control. - /// The mouse position in the control's coordinates. - public Point GetPosition(IVisual relativeTo) - { - relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo)); - - if (relativeTo.VisualRoot == null) - { - throw new InvalidOperationException("Control is not attached to visual tree."); - } - -#pragma warning disable CS0618 // Type or member is obsolete - var rootPoint = relativeTo.VisualRoot.PointToClient(Position); -#pragma warning restore CS0618 // Type or member is obsolete - var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); - return rootPoint * transform!.Value; - } - - public void ProcessRawEvent(RawInputEventArgs e) - { - if (!e.Handled && e is RawPointerEventArgs margs) - ProcessRawEvent(margs); - } - - public void TopLevelClosed(IInputRoot root) - { - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - } - - public void SceneInvalidated(IInputRoot root, Rect rect) - { - // Pointer is outside of the target area - if (_position == null ) - { - if (root.PointerOverElement != null) - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - return; - } - - - var clientPoint = root.PointToClient(_position.Value); - - if (rect.Contains(clientPoint)) - { - if (_pointer.Captured == null) - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, - PointerPointProperties.None, KeyModifiers.None); - } - else - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, - PointerPointProperties.None, KeyModifiers.None); - } - } - } - - int ButtonCount(PointerPointProperties props) - { - var rv = 0; - if (props.IsLeftButtonPressed) - rv++; - if (props.IsMiddleButtonPressed) - rv++; - if (props.IsRightButtonPressed) - rv++; - if (props.IsXButton1Pressed) - rv++; - if (props.IsXButton2Pressed) - rv++; - return rv; - } - - private void ProcessRawEvent(RawPointerEventArgs e) - { - e = e ?? throw new ArgumentNullException(nameof(e)); - - var mouse = (MouseDevice)e.Device; - if(mouse._disposed) - return; - - if (e.Type == RawPointerEventType.NonClientLeftButtonDown) return; - - _position = e.Root.PointToScreen(e.Position); - var props = CreateProperties(e); - var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); - switch (e.Type) - { - case RawPointerEventType.LeaveWindow: - LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); - break; - case RawPointerEventType.LeftButtonDown: - case RawPointerEventType.RightButtonDown: - case RawPointerEventType.MiddleButtonDown: - case RawPointerEventType.XButton1Down: - case RawPointerEventType.XButton2Down: - if (ButtonCount(props) > 1) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); - else - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - props, keyModifiers); - break; - case RawPointerEventType.LeftButtonUp: - case RawPointerEventType.RightButtonUp: - case RawPointerEventType.MiddleButtonUp: - case RawPointerEventType.XButton1Up: - case RawPointerEventType.XButton2Up: - if (ButtonCount(props) != 0) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); - else - e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); - break; - case RawPointerEventType.Move: - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); - break; - case RawPointerEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); - break; - case RawPointerEventType.Magnify: - e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); - break; - case RawPointerEventType.Rotate: - e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); - break; - case RawPointerEventType.Swipe: - e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); - break; - } - } - - private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - _position = null; - ClearPointerOver(this, timestamp, root, properties, inputModifiers); - } - - - PointerPointProperties CreateProperties(RawPointerEventArgs args) - { - - var kind = PointerUpdateKind.Other; - - if (args.Type == RawPointerEventType.LeftButtonDown) - kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.MiddleButtonDown) - kind = PointerUpdateKind.MiddleButtonPressed; - if (args.Type == RawPointerEventType.RightButtonDown) - kind = PointerUpdateKind.RightButtonPressed; - if (args.Type == RawPointerEventType.XButton1Down) - kind = PointerUpdateKind.XButton1Pressed; - if (args.Type == RawPointerEventType.XButton2Down) - kind = PointerUpdateKind.XButton2Pressed; - if (args.Type == RawPointerEventType.LeftButtonUp) - kind = PointerUpdateKind.LeftButtonReleased; - if (args.Type == RawPointerEventType.MiddleButtonUp) - kind = PointerUpdateKind.MiddleButtonReleased; - if (args.Type == RawPointerEventType.RightButtonUp) - kind = PointerUpdateKind.RightButtonReleased; - if (args.Type == RawPointerEventType.XButton1Up) - kind = PointerUpdateKind.XButton1Released; - if (args.Type == RawPointerEventType.XButton2Up) - kind = PointerUpdateKind.XButton2Released; - - return new PointerPointProperties(args.InputModifiers, kind); - } - - private MouseButton _lastMouseDownButton; - private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - _pointer.Capture(hit); - var source = GetSource(hit); - if (source != null) - { - var settings = AvaloniaLocator.Current.GetService(); - var doubleClickTime = settings?.DoubleClickTime.TotalMilliseconds ?? 500; - var doubleClickSize = settings?.DoubleClickSize ?? new Size(4, 4); - - if (!_lastClickRect.Contains(p) || timestamp - _lastClickTime > doubleClickTime) - { - _clickCount = 0; - } - - ++_clickCount; - _lastClickTime = timestamp; - _lastClickRect = new Rect(p, new Size()) - .Inflate(new Thickness(doubleClickSize.Width / 2, doubleClickSize.Height / 2)); - _lastMouseDownButton = properties.PointerUpdateKind.GetMouseButton(); - var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); - source.RaiseEvent(e); - return e.Handled; - } - } - - return false; - } - - private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers, Lazy?>? intermediatePoints) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - IInputElement? source; - - if (_pointer.Captured == null) - { - source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); - } - else - { - SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); - source = _pointer.Captured; - } - - if (source is object) - { - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, - p, timestamp, properties, inputModifiers, intermediatePoints); - - source.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - if (source is not null) - { - var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, - _lastMouseDownButton); - - source?.RaiseEvent(e); - _pointer.Capture(null); - return e.Handled; - } - - return false; - } - - private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - var source = GetSource(hit); - - // KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform. - // If Shift-Key is pressed and X is close to 0 we swap the Vector. - if (inputModifiers == KeyModifiers.Shift && MathUtilities.IsZero(delta.X)) - { - delta = new Vector(delta.Y, delta.X); - } - - if (source is not null) - { - var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - var source = GetSource(hit); - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source, - _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - var source = GetSource(hit); - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source, - _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var hit = HitTest(root, p); - - if (hit != null) - { - var source = GetSource(hit); - var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source, - _pointer, root, p, timestamp, props, inputModifiers, delta); - - source?.RaiseEvent(e); - return e.Handled; - } - - return false; - } - - private IInteractive? GetSource(IVisual? hit) - { - if (hit is null) - return null; - - return _pointer.Captured ?? - (hit as IInteractive) ?? - hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - } - - private IInputElement? HitTest(IInputElement root, Point p) - { - root = root ?? throw new ArgumentNullException(nameof(root)); - - return _pointer.Captured ?? root.InputHitTest(p); - } - - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - return new PointerEventArgs(ev, source, _pointer, null, default, - timestamp, properties, inputModifiers); - } - - private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); - - if (element!=null && !element.IsAttachedToVisualTree) - { - // element has been removed from visual tree so do top down cleanup - if (root.IsPointerOver) - ClearChildrenPointerOver(e, root,true); - } - while (element != null) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - element = (IInputElement?)element.VisualParent; - } - - root.PointerOverElement = null; - } - - private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) - { - foreach (IInputElement el in element.VisualChildren) - { - if (el.IsPointerOver) - { - ClearChildrenPointerOver(e, el, true); - break; - } - } - if(clearRoot) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - } - } - - private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.InputHitTest(p); - - if (element != root.PointerOverElement) - { - if (element != null) - { - SetPointerOver(device, timestamp, root, element, properties, inputModifiers); - } - else - { - ClearPointerOver(device, timestamp, root, properties, inputModifiers); - } - } - - return element; - } - - private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - element = element ?? throw new ArgumentNullException(nameof(element)); - - IInputElement? branch = null; - - IInputElement? el = element; - - while (el != null) - { - if (el.IsPointerOver) - { - branch = el; - break; - } - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement; - - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); - if (el!=null && branch!=null && !el.IsAttachedToVisualTree) - { - ClearChildrenPointerOver(e,branch,false); - } - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement = element; - e.RoutedEvent = InputElement.PointerEnterEvent; - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - } - - public void Dispose() - { - _disposed = true; - _pointer?.Dispose(); - } - } -} diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs deleted file mode 100644 index c8290cb3b7..0000000000 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ /dev/null @@ -1,673 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.VisualTree; - -namespace Avalonia.Input.Navigation -{ - /// - /// The implementation for default tab navigation. - /// - internal static class TabNavigation - { - public static IInputElement? GetNextTab(IInputElement e, bool goDownOnly) - { - return GetNextTab(e, GetGroupParent(e), goDownOnly); - } - - public static IInputElement? GetNextTab(IInputElement? e, IInputElement container, bool goDownOnly) - { - var tabbingType = GetKeyNavigationMode(container); - - if (e == null) - { - if (IsTabStop(container)) - return container; - - // Using ActiveElement if set - var activeElement = GetActiveElement(container); - if (activeElement != null) - return GetNextTab(null, activeElement, true); - } - else - { - if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None) - { - if (container != e) - { - if (goDownOnly) - return null; - var parentContainer = GetGroupParent(container); - return GetNextTab(container, parentContainer, goDownOnly); - } - } - } - - // All groups - IInputElement? loopStartElement = null; - var nextTabElement = e; - var currentTabbingType = tabbingType; - - // Search down inside the container - while ((nextTabElement = GetNextTabInGroup(nextTabElement, container, currentTabbingType)) != null) - { - // Avoid the endless loop here for Cycle groups - if (loopStartElement == nextTabElement) - break; - if (loopStartElement == null) - loopStartElement = nextTabElement; - - var firstTabElementInside = GetNextTab(null, nextTabElement, true); - if (firstTabElementInside != null) - return firstTabElementInside; - - // If we want to continue searching inside the Once groups, we should change the navigation mode - if (currentTabbingType == KeyboardNavigationMode.Once) - currentTabbingType = KeyboardNavigationMode.Contained; - } - - // If there is no next element in the group (nextTabElement == null) - - // Search up in the tree if allowed - // consider: Use original tabbingType instead of currentTabbingType - if (!goDownOnly && currentTabbingType != KeyboardNavigationMode.Contained && GetParent(container) != null) - { - return GetNextTab(container, GetGroupParent(container), false); - } - - return null; - } - - public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e) - { - if (e is IInputElement container) - { - var last = GetLastInTree(container); - - if (last is object) - return GetNextTab(last, false); - } - - return null; - } - - public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly) - { - if (e is null && container is null) - throw new InvalidOperationException("Either 'e' or 'container' must be non-null."); - - if (container is null) - container = GetGroupParent(e!); - - KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container); - - if (e == null) - { - // Using ActiveElement if set - var activeElement = GetActiveElement(container); - if (activeElement != null) - return GetPrevTab(null, activeElement, true); - else - { - // If we Shift+Tab on a container with KeyboardNavigationMode=Once, and ActiveElement is null - // then we want to go to the first item (not last) within the container - if (tabbingType == KeyboardNavigationMode.Once) - { - var firstTabElement = GetNextTabInGroup(null, container, tabbingType); - if (firstTabElement == null) - { - if (IsTabStop(container)) - return container; - if (goDownOnly) - return null; - - return GetPrevTab(container, null, false); - } - else - { - return GetPrevTab(null, firstTabElement, true); - } - } - } - } - else - { - if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None) - { - if (goDownOnly || container == e) - return null; - - // FocusedElement should not be e otherwise we will delegate focus to the same element - if (IsTabStop(container)) - return container; - - return GetPrevTab(container, null, false); - } - } - - // All groups (except Once) - continue - IInputElement? loopStartElement = null; - IInputElement? nextTabElement = e; - - // Look for element with the same TabIndex before the current element - while ((nextTabElement = GetPrevTabInGroup(nextTabElement, container, tabbingType)) != null) - { - if (nextTabElement == container && tabbingType == KeyboardNavigationMode.Local) - break; - - // At this point nextTabElement is TabStop or TabGroup - // In case it is a TabStop only return the element - if (IsTabStop(nextTabElement) && !IsGroup(nextTabElement)) - return nextTabElement; - - // Avoid the endless loop here - if (loopStartElement == nextTabElement) - break; - if (loopStartElement == null) - loopStartElement = nextTabElement; - - // At this point nextTabElement is TabGroup - var lastTabElementInside = GetPrevTab(null, nextTabElement, true); - if (lastTabElementInside != null) - return lastTabElementInside; - } - - if (tabbingType == KeyboardNavigationMode.Contained) - return null; - - if (e != container && IsTabStop(container)) - return container; - - // If end of the subtree is reached or there no other elements above - if (!goDownOnly && GetParent(container) != null) - { - return GetPrevTab(container, null, false); - } - - return null; - } - - public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e) - { - if (e is IInputElement container) - { - var first = GetFirstChild(container); - - if (first is object) - return GetPrevTab(first, null, false); - } - - return null; - } - - private static IInputElement? FocusedElement(IInputElement e) - { - var iie = e; - // Focus delegation is enabled only if keyboard focus is outside the container - if (iie != null && !iie.IsKeyboardFocusWithin) - { - var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e); - if (focusedElement != null) - { - if (!IsFocusScope(e)) - { - // Verify if focusedElement is a visual descendant of e - if (focusedElement is IVisual visualFocusedElement && - visualFocusedElement != e && - e.IsVisualAncestorOf(visualFocusedElement)) - { - return focusedElement; - } - } - } - } - - return null; - } - - private static IInputElement? GetFirstChild(IInputElement e) - { - // If the element has a FocusedElement it should be its first child - if (FocusedElement(e) is IInputElement focusedElement) - return focusedElement; - - // Return the first visible element. - var uiElement = e as InputElement; - - if (uiElement is null || IsVisibleAndEnabled(uiElement)) - { - if (e is IVisual elementAsVisual) - { - var children = elementAsVisual.VisualChildren; - var count = children.Count; - - for (int i = 0; i < count; i++) - { - if (children[i] is InputElement ie) - { - if (IsVisibleAndEnabled(ie)) - return ie; - else - { - var firstChild = GetFirstChild(ie); - if (firstChild != null) - return firstChild; - } - } - } - } - } - - return null; - } - - private static IInputElement? GetLastChild(IInputElement e) - { - // If the element has a FocusedElement it should be its last child - if (FocusedElement(e) is IInputElement focusedElement) - return focusedElement; - - // Return the last visible element. - var uiElement = e as InputElement; - - if (uiElement == null || IsVisibleAndEnabled(uiElement)) - { - var elementAsVisual = e as IVisual; - - if (elementAsVisual != null) - { - var children = elementAsVisual.VisualChildren; - var count = children.Count; - - for (int i = count - 1; i >= 0; i--) - { - if (children[i] is InputElement ie) - { - if (IsVisibleAndEnabled(ie)) - return ie; - else - { - var lastChild = GetLastChild(ie); - if (lastChild != null) - return lastChild; - } - } - } - } - } - - return null; - } - - private static IInputElement? GetFirstTabInGroup(IInputElement container) - { - IInputElement? firstTabElement = null; - int minIndexFirstTab = int.MinValue; - - var currElement = container; - while ((currElement = GetNextInTree(currElement, container)) != null) - { - if (IsTabStopOrGroup(currElement)) - { - int currPriority = KeyboardNavigation.GetTabIndex(currElement); - - if (currPriority < minIndexFirstTab || firstTabElement == null) - { - minIndexFirstTab = currPriority; - firstTabElement = currElement; - } - } - } - return firstTabElement; - } - - private static IInputElement? GetLastInTree(IInputElement container) - { - IInputElement? result; - IInputElement? c = container; - - do - { - result = c; - c = GetLastChild(c); - } while (c != null && !IsGroup(c)); - - if (c != null) - return c; - - return result; - } - - private static IInputElement? GetLastTabInGroup(IInputElement container) - { - IInputElement? lastTabElement = null; - int maxIndexFirstTab = int.MaxValue; - var currElement = GetLastInTree(container); - while (currElement != null && currElement != container) - { - if (IsTabStopOrGroup(currElement)) - { - int currPriority = KeyboardNavigation.GetTabIndex(currElement); - - if (currPriority > maxIndexFirstTab || lastTabElement == null) - { - maxIndexFirstTab = currPriority; - lastTabElement = currElement; - } - } - currElement = GetPreviousInTree(currElement, container); - } - return lastTabElement; - } - - private static IInputElement? GetNextInTree(IInputElement e, IInputElement container) - { - IInputElement? result = null; - - if (e == container || !IsGroup(e)) - result = GetFirstChild(e); - - if (result != null || e == container) - return result; - - IInputElement? parent = e; - do - { - var sibling = GetNextSibling(parent); - if (sibling != null) - return sibling; - - parent = GetParent(parent); - } while (parent != null && parent != container); - - return null; - } - - private static IInputElement? GetNextSibling(IInputElement e) - { - if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual) - { - var children = parentAsVisual.VisualChildren; - var count = children.Count; - var i = 0; - - //go till itself - for (; i < count; i++) - { - var vchild = children[i]; - if (vchild == elementAsVisual) - break; - } - i++; - //search ahead - for (; i < count; i++) - { - var visual = children[i]; - if (visual is IInputElement ie) - return ie; - } - } - - return null; - } - - private static IInputElement? GetNextTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType) - { - // None groups: Tab navigation is not supported - if (tabbingType == KeyboardNavigationMode.None) - return null; - - // e == null or e == container -> return the first TabStopOrGroup - if (e == null || e == container) - { - return GetFirstTabInGroup(container); - } - - if (tabbingType == KeyboardNavigationMode.Once) - return null; - - var nextTabElement = GetNextTabWithSameIndex(e, container); - if (nextTabElement != null) - return nextTabElement; - - return GetNextTabWithNextIndex(e, container, tabbingType); - } - - private static IInputElement? GetNextTabWithSameIndex(IInputElement e, IInputElement container) - { - var elementTabPriority = KeyboardNavigation.GetTabIndex(e); - var currElement = e; - while ((currElement = GetNextInTree(currElement, container)) != null) - { - if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority) - { - return currElement; - } - } - - return null; - } - - private static IInputElement? GetNextTabWithNextIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType) - { - // Find the next min index in the tree - // min (index>currentTabIndex) - IInputElement? nextTabElement = null; - IInputElement? firstTabElement = null; - int minIndexFirstTab = int.MinValue; - int minIndex = int.MinValue; - int elementTabPriority = KeyboardNavigation.GetTabIndex(e); - - IInputElement? currElement = container; - while ((currElement = GetNextInTree(currElement, container)) != null) - { - if (IsTabStopOrGroup(currElement)) - { - int currPriority = KeyboardNavigation.GetTabIndex(currElement); - if (currPriority > elementTabPriority) - { - if (currPriority < minIndex || nextTabElement == null) - { - minIndex = currPriority; - nextTabElement = currElement; - } - } - - if (currPriority < minIndexFirstTab || firstTabElement == null) - { - minIndexFirstTab = currPriority; - firstTabElement = currElement; - } - } - } - - // Cycle groups: if not found - return first element - if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null) - nextTabElement = firstTabElement; - - return nextTabElement; - } - - private static IInputElement? GetPrevTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType) - { - // None groups: Tab navigation is not supported - if (tabbingType == KeyboardNavigationMode.None) - return null; - - // Search the last index inside the group - if (e == null) - { - return GetLastTabInGroup(container); - } - - if (tabbingType == KeyboardNavigationMode.Once) - return null; - - if (e == container) - return null; - - var nextTabElement = GetPrevTabWithSameIndex(e, container); - if (nextTabElement != null) - return nextTabElement; - - return GetPrevTabWithPrevIndex(e, container, tabbingType); - } - - private static IInputElement? GetPrevTabWithSameIndex(IInputElement e, IInputElement container) - { - int elementTabPriority = KeyboardNavigation.GetTabIndex(e); - var currElement = GetPreviousInTree(e, container); - while (currElement != null) - { - if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority && currElement != container) - { - return currElement; - } - currElement = GetPreviousInTree(currElement, container); - } - return null; - } - - private static IInputElement? GetPrevTabWithPrevIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType) - { - // Find the next max index in the tree - // max (index maxIndex || nextTabElement == null) - { - maxIndex = currPriority; - nextTabElement = currElement; - } - } - - if (currPriority > maxIndexFirstTab || lastTabElement == null) - { - maxIndexFirstTab = currPriority; - lastTabElement = currElement; - } - } - - currElement = GetPreviousInTree(currElement, container); - } - - // Cycle groups: if not found - return first element - if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null) - nextTabElement = lastTabElement; - - return nextTabElement; - } - - private static IInputElement? GetPreviousInTree(IInputElement e, IInputElement container) - { - if (e == container) - return null; - - var result = GetPreviousSibling(e); - - if (result != null) - { - if (IsGroup(result)) - return result; - else - return GetLastInTree(result); - } - else - return GetParent(e); - } - - private static IInputElement? GetPreviousSibling(IInputElement e) - { - if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual) - { - var children = parentAsVisual.VisualChildren; - var count = children.Count; - IInputElement? prev = null; - - for (int i = 0; i < count; i++) - { - var vchild = children[i]; - if (vchild == elementAsVisual) - break; - if (vchild is IInputElement ie && IsVisibleAndEnabled(ie)) - prev = ie; - } - return prev; - } - return null; - } - - private static IInputElement? GetActiveElement(IInputElement e) - { - return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabOnceActiveElementProperty); - } - - private static IInputElement GetGroupParent(IInputElement e) => GetGroupParent(e, false); - - private static IInputElement GetGroupParent(IInputElement element, bool includeCurrent) - { - var result = element; // Keep the last non null element - var e = element; - - // If we don't want to include the current element, - // start at the parent of the element. If the element - // is the root, then just return it as the group parent. - if (!includeCurrent) - { - result = e; - e = GetParent(e); - if (e == null) - return result; - } - - while (e != null) - { - if (IsGroup(e)) - return e; - - result = e; - e = GetParent(e); - } - - return result; - } - - private static IInputElement? GetParent(IInputElement e) - { - // For Visual - go up the visual parent chain until we find Visual. - if (e is IVisual v) - return v.FindAncestorOfType(); - - // This will need to be implemented when we have non-visual input elements. - throw new NotSupportedException(); - } - - private static KeyboardNavigationMode GetKeyNavigationMode(IInputElement e) - { - return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabNavigationProperty); - } - private static bool IsFocusScope(IInputElement e) => FocusManager.GetIsFocusScope(e) || GetParent(e) == null; - private static bool IsGroup(IInputElement e) => GetKeyNavigationMode(e) != KeyboardNavigationMode.Continue; - - private static bool IsTabStop(IInputElement e) - { - if (e is InputElement ie) - return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEnabled; - return false; - } - - private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e); - private static bool IsVisibleAndEnabled(IInputElement e) => e.IsVisible && e.IsEnabled; - } -} diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs deleted file mode 100644 index 0604d09dc4..0000000000 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Input.Raw; -using Avalonia.Interactivity; -using Avalonia.VisualTree; - -namespace Avalonia.Input -{ - public class PointerEventArgs : RoutedEventArgs - { - private readonly IVisual? _rootVisual; - private readonly Point _rootVisualPosition; - private readonly PointerPointProperties _properties; - private Lazy?>? _previousPoints; - - public PointerEventArgs(RoutedEvent routedEvent, - IInteractive? source, - IPointer pointer, - IVisual? rootVisual, Point rootVisualPosition, - ulong timestamp, - PointerPointProperties properties, - KeyModifiers modifiers) - : base(routedEvent) - { - Source = source; - _rootVisual = rootVisual; - _rootVisualPosition = rootVisualPosition; - _properties = properties; - Pointer = pointer; - Timestamp = timestamp; - KeyModifiers = modifiers; - } - - public PointerEventArgs(RoutedEvent routedEvent, - IInteractive? source, - IPointer pointer, - IVisual? rootVisual, Point rootVisualPosition, - ulong timestamp, - PointerPointProperties properties, - KeyModifiers modifiers, - Lazy?>? previousPoints) - : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) - { - _previousPoints = previousPoints; - } - - - class EmulatedDevice : IPointerDevice - { - private readonly PointerEventArgs _ev; - - public EmulatedDevice(PointerEventArgs ev) - { - _ev = ev; - } - - public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException(); - - public IInputElement? Captured => _ev.Pointer.Captured; - public void Capture(IInputElement? control) - { - _ev.Pointer.Capture(control); - } - - public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); - } - - public IPointer Pointer { get; } - public ulong Timestamp { get; } - - private IPointerDevice? _device; - - [Obsolete("Use Pointer to get pointer-specific information")] - public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this)); - - [Obsolete("Use KeyModifiers and PointerPointProperties")] - public InputModifiers InputModifiers - { - get - { - var mods = (InputModifiers)KeyModifiers; - if (_properties.IsLeftButtonPressed) - mods |= InputModifiers.LeftMouseButton; - if (_properties.IsMiddleButtonPressed) - mods |= InputModifiers.MiddleMouseButton; - if (_properties.IsRightButtonPressed) - mods |= InputModifiers.RightMouseButton; - - return mods; - } - } - - public KeyModifiers KeyModifiers { get; } - - private Point GetPosition(Point pt, IVisual? relativeTo) - { - if (_rootVisual == null) - return default; - if (relativeTo == null) - return pt; - return pt * _rootVisual.TransformToVisual(relativeTo) ?? default; - } - - public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); - - [Obsolete("Use GetCurrentPoint")] - public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo); - - /// - /// Returns the PointerPoint associated with the current event - /// - /// The visual which coordinate system to use. Pass null for toplevel coordinate system - /// - public PointerPoint GetCurrentPoint(IVisual? relativeTo) - => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); - - /// - /// Returns the PointerPoint associated with the current event - /// - /// The visual which coordinate system to use. Pass null for toplevel coordinate system - /// - public IReadOnlyList GetIntermediatePoints(IVisual? relativeTo) - { - var previousPoints = _previousPoints?.Value; - if (previousPoints == null || previousPoints.Count == 0) - return new[] { GetCurrentPoint(relativeTo) }; - var points = new PointerPoint[previousPoints.Count + 1]; - for (var c = 0; c < previousPoints.Count; c++) - { - var pt = previousPoints[c]; - points[c] = new PointerPoint(Pointer, GetPosition(pt.Position, relativeTo), _properties); - } - - points[points.Length - 1] = GetCurrentPoint(relativeTo); - return points; - } - - /// - /// Returns the current pointer point properties - /// - protected PointerPointProperties Properties => _properties; - } - - public enum MouseButton - { - None, - Left, - Right, - Middle, - XButton1, - XButton2 - } - - public class PointerPressedEventArgs : PointerEventArgs - { - private readonly int _clickCount; - - public PointerPressedEventArgs( - IInteractive source, - IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, - ulong timestamp, - PointerPointProperties properties, - KeyModifiers modifiers, - int clickCount = 1) - : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, - timestamp, properties, modifiers) - { - _clickCount = clickCount; - } - - public int ClickCount => _clickCount; - - [Obsolete("Use PointerPressedEventArgs.GetCurrentPoint(this).Properties")] - public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton(); - } - - public class PointerReleasedEventArgs : PointerEventArgs - { - public PointerReleasedEventArgs( - IInteractive source, IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, ulong timestamp, - PointerPointProperties properties, KeyModifiers modifiers, - MouseButton initialPressMouseButton) - : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, - timestamp, properties, modifiers) - { - InitialPressMouseButton = initialPressMouseButton; - } - - /// - /// Gets the mouse button that triggered the corresponding PointerPressed event - /// - public MouseButton InitialPressMouseButton { get; } - - [Obsolete("Use InitialPressMouseButton")] - public MouseButton MouseButton => InitialPressMouseButton; - } - - public class PointerCaptureLostEventArgs : RoutedEventArgs - { - public IPointer Pointer { get; } - - public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) - { - Pointer = pointer; - Source = source; - } - } -} diff --git a/src/Avalonia.Input/Properties/AssemblyInfo.cs b/src/Avalonia.Input/Properties/AssemblyInfo.cs deleted file mode 100644 index 6a68bf60d1..0000000000 --- a/src/Avalonia.Input/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Reflection; -using Avalonia.Metadata; - -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.TextInput")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")] diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs deleted file mode 100644 index 6e9ce20ff1..0000000000 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace Avalonia.Input.Raw -{ - public class RawDragEvent : RawInputEventArgs - { - public Point Location { get; set; } - public IDataObject Data { get; } - public DragDropEffects Effects { get; set; } - public RawDragEventType Type { get; } - [Obsolete("Use KeyModifiers")] - public InputModifiers Modifiers { get; } - public KeyModifiers KeyModifiers { get; } - - public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, - IInputRoot root, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) - :base(inputDevice, 0, root) - { - Type = type; - Location = location; - Data = data; - Effects = effects; - KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); -#pragma warning disable CS0618 // Type or member is obsolete - Modifiers = (InputModifiers)modifiers; -#pragma warning restore CS0618 // Type or member is obsolete - } - } -} diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs deleted file mode 100644 index c157fa059c..0000000000 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Avalonia.Input.Raw -{ - public enum RawPointerEventType - { - LeaveWindow, - LeftButtonDown, - LeftButtonUp, - RightButtonDown, - RightButtonUp, - MiddleButtonDown, - MiddleButtonUp, - XButton1Down, - XButton1Up, - XButton2Down, - XButton2Up, - Move, - Wheel, - NonClientLeftButtonDown, - TouchBegin, - TouchUpdate, - TouchEnd, - TouchCancel, - Magnify, - Rotate, - Swipe - } - - /// - /// A raw mouse event. - /// - public class RawPointerEventArgs : RawInputEventArgs - { - private RawPointerPoint _point; - - /// - /// Initializes a new instance of the class. - /// - /// The associated device. - /// The event timestamp. - /// The root from which the event originates. - /// The type of the event. - /// The mouse position, in client DIPs. - /// The input modifiers. - public RawPointerEventArgs( - IInputDevice device, - ulong timestamp, - IInputRoot root, - RawPointerEventType type, - Point position, - RawInputModifiers inputModifiers) - : base(device, timestamp, root) - { - Contract.Requires(device != null); - Contract.Requires(root != null); - - Position = position; - Type = type; - InputModifiers = inputModifiers; - } - - /// - /// Initializes a new instance of the class. - /// - /// The associated device. - /// The event timestamp. - /// The root from which the event originates. - /// The type of the event. - /// The point properties and position, in client DIPs. - /// The input modifiers. - public RawPointerEventArgs( - IInputDevice device, - ulong timestamp, - IInputRoot root, - RawPointerEventType type, - RawPointerPoint point, - RawInputModifiers inputModifiers) - : base(device, timestamp, root) - { - Contract.Requires(device != null); - Contract.Requires(root != null); - - Point = point; - Type = type; - InputModifiers = inputModifiers; - } - - /// - /// Gets the pointer properties and position, in client DIPs. - /// - public RawPointerPoint Point - { - get => _point; - set => _point = value; - } - - /// - /// Gets the mouse position, in client DIPs. - /// - public Point Position - { - get => _point.Position; - set => _point.Position = value; - } - - /// - /// Gets the type of the event. - /// - public RawPointerEventType Type { get; set; } - - /// - /// Gets the input modifiers. - /// - public RawInputModifiers InputModifiers { get; set; } - - /// - /// Points that were traversed by a pointer since the previous relevant event, - /// only valid for Move and TouchUpdate - /// - public Lazy?>? IntermediatePoints { get; set; } - } - - public struct RawPointerPoint - { - /// - /// Pointer position, in client DIPs. - /// - public Point Position { get; set; } - - public RawPointerPoint() - { - Position = default; - } - } -} diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs deleted file mode 100644 index ed7ec5465c..0000000000 --- a/src/Avalonia.Input/TouchDevice.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Input.Raw; -using Avalonia.Platform; - -namespace Avalonia.Input -{ - /// - /// Handles raw touch events - /// - /// This class is supposed to be used on per-toplevel basis, don't use a shared one - /// - /// - public class TouchDevice : IInputDevice, IDisposable - { - private readonly Dictionary _pointers = new Dictionary(); - private bool _disposed; - private int _clickCount; - private Rect _lastClickRect; - private ulong _lastClickTime; - KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => - (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); - - RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown) - { - var rv = modifiers &= RawInputModifiers.KeyboardMask; - if (isLeftButtonDown) - rv |= RawInputModifiers.LeftMouseButton; - return rv; - } - - public void ProcessRawEvent(RawInputEventArgs ev) - { - if (_disposed) - return; - var args = (RawTouchEventArgs)ev; - if (!_pointers.TryGetValue(args.TouchPointId, out var pointer)) - { - if (args.Type == RawPointerEventType.TouchEnd) - return; - var hit = args.Root.InputHitTest(args.Position); - - _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), - PointerType.Touch, _pointers.Count == 0); - pointer.Capture(hit); - } - - - var target = pointer.Captured ?? args.Root; - if (args.Type == RawPointerEventType.TouchBegin) - { - if (_pointers.Count > 1) - { - _clickCount = 1; - _lastClickTime = 0; - _lastClickRect = new Rect(); - } - else - { - var settings = AvaloniaLocator.Current.GetRequiredService(); - - if (!_lastClickRect.Contains(args.Position) - || ev.Timestamp - _lastClickTime > settings.TouchDoubleClickTime.TotalMilliseconds) - { - _clickCount = 0; - } - ++_clickCount; - _lastClickTime = ev.Timestamp; - _lastClickRect = new Rect(args.Position, new Size()) - .Inflate(new Thickness(settings.TouchDoubleClickSize.Width / 2, settings.TouchDoubleClickSize.Height / 2)); - } - - target.RaiseEvent(new PointerPressedEventArgs(target, pointer, - args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, true), - PointerUpdateKind.LeftButtonPressed), - GetKeyModifiers(args.InputModifiers), _clickCount)); - } - - if (args.Type == RawPointerEventType.TouchEnd) - { - _pointers.Remove(args.TouchPointId); - using (pointer) - { - target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, - args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, false), - PointerUpdateKind.LeftButtonReleased), - GetKeyModifiers(args.InputModifiers), MouseButton.Left)); - } - } - - if (args.Type == RawPointerEventType.TouchCancel) - { - _pointers.Remove(args.TouchPointId); - using (pointer) - pointer.Capture(null); - } - - if (args.Type == RawPointerEventType.TouchUpdate) - { - var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); - target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, - args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other), - GetKeyModifiers(args.InputModifiers), args.IntermediatePoints)); - } - - - } - - public void Dispose() - { - if (_disposed) - return; - var values = _pointers.Values.ToArray(); - _pointers.Clear(); - _disposed = true; - foreach (var p in values) - p.Dispose(); - } - - } -} diff --git a/src/Avalonia.Interactivity/ApiCompatBaseline.txt b/src/Avalonia.Interactivity/ApiCompatBaseline.txt deleted file mode 100644 index fcc74cf864..0000000000 --- a/src/Avalonia.Interactivity/ApiCompatBaseline.txt +++ /dev/null @@ -1 +0,0 @@ -Total Issues: 0 diff --git a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj deleted file mode 100644 index 03a53f7d86..0000000000 --- a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - net6.0;netstandard2.0 - - - - - - - - - - - \ No newline at end of file diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs deleted file mode 100644 index afda29e329..0000000000 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; - -#nullable enable - -namespace Avalonia.Interactivity -{ - /// - /// Interface for objects that raise routed events. - /// - public interface IInteractive - { - /// - /// Gets the interactive parent of the object for bubbling and tunneling events. - /// - IInteractive? InteractiveParent { get; } - - /// - /// Adds a handler for the specified routed event. - /// - /// The routed event. - /// The handler. - /// The routing strategies to listen to. - /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - void AddHandler( - RoutedEvent routedEvent, - Delegate handler, - RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, - bool handledEventsToo = false); - - /// - /// Adds a handler for the specified routed event. - /// - /// The type of the event's args. - /// The routed event. - /// The handler. - /// The routing strategies to listen to. - /// Whether handled events should also be listened for. - /// A disposable that terminates the event subscription. - void AddHandler( - RoutedEvent routedEvent, - EventHandler handler, - RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, - bool handledEventsToo = false) where TEventArgs : RoutedEventArgs; - - /// - /// Removes a handler for the specified routed event. - /// - /// The routed event. - /// The handler. - void RemoveHandler(RoutedEvent routedEvent, Delegate handler); - - /// - /// Removes a handler for the specified routed event. - /// - /// The type of the event's args. - /// The routed event. - /// The handler. - void RemoveHandler(RoutedEvent routedEvent, EventHandler handler) - where TEventArgs : RoutedEventArgs; - - /// - /// Adds the object's handlers for a routed event to an event route. - /// - /// The event. - /// The event route. - void AddToEventRoute(RoutedEvent routedEvent, EventRoute route); - - /// - /// Raises a routed event. - /// - /// The event args. - void RaiseEvent(RoutedEventArgs e); - } -} diff --git a/src/Avalonia.Layout/ApiCompatBaseline.txt b/src/Avalonia.Layout/ApiCompatBaseline.txt deleted file mode 100644 index fcc74cf864..0000000000 --- a/src/Avalonia.Layout/ApiCompatBaseline.txt +++ /dev/null @@ -1 +0,0 @@ -Total Issues: 0 diff --git a/src/Avalonia.Layout/Avalonia.Layout.csproj b/src/Avalonia.Layout/Avalonia.Layout.csproj deleted file mode 100644 index f2c4b0540b..0000000000 --- a/src/Avalonia.Layout/Avalonia.Layout.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net6.0;netstandard2.0 - - - - - - - - - - diff --git a/src/Avalonia.Layout/Properties/AssemblyInfo.cs b/src/Avalonia.Layout/Properties/AssemblyInfo.cs deleted file mode 100644 index efcbf184b5..0000000000 --- a/src/Avalonia.Layout/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Runtime.CompilerServices; -using Avalonia.Metadata; - -[assembly: InternalsVisibleTo("Avalonia.Layout.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")] - diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs deleted file mode 100644 index c63fe5e405..0000000000 --- a/src/Avalonia.Layout/StackLayout.cs +++ /dev/null @@ -1,363 +0,0 @@ -// This source file is adapted from the WinUI project. -// (https://github.com/microsoft/microsoft-ui-xaml) -// -// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. - -using System; -using System.Collections.Specialized; -using Avalonia.Data; -using Avalonia.Logging; - -namespace Avalonia.Layout -{ - /// - /// Arranges elements into a single line (with spacing) that can be oriented horizontally or vertically. - /// - public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates - { - /// - /// Defines the property. - /// - public static readonly StyledProperty DisableVirtualizationProperty = - AvaloniaProperty.Register(nameof(DisableVirtualization)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); - - /// - /// Defines the property. - /// - public static readonly StyledProperty SpacingProperty = - AvaloniaProperty.Register(nameof(Spacing)); - - private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); - - /// - /// Initializes a new instance of the StackLayout class. - /// - public StackLayout() - { - LayoutId = "StackLayout"; - } - - /// - /// Gets or sets a value indicating whether virtualization is disabled on the layout. - /// - public bool DisableVirtualization - { - get => GetValue(DisableVirtualizationProperty); - set => SetValue(DisableVirtualizationProperty, value); - } - - /// - /// Gets or sets the axis along which items are laid out. - /// - /// - /// One of the enumeration values that specifies the axis along which items are laid out. - /// The default is Vertical. - /// - public Orientation Orientation - { - get => GetValue(OrientationProperty); - set => SetValue(OrientationProperty, value); - } - - /// - /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the - /// direction of the StackLayout's Orientation. - /// - public double Spacing - { - get => GetValue(SpacingProperty); - set => SetValue(SpacingProperty, value); - } - - internal Rect GetExtent( - Size availableSize, - VirtualizingLayoutContext context, - ILayoutable? firstRealized, - int firstRealizedItemIndex, - Rect firstRealizedLayoutBounds, - ILayoutable? lastRealized, - int lastRealizedItemIndex, - Rect lastRealizedLayoutBounds) - { - var extent = new Rect(); - - // Constants - int itemsCount = context.ItemCount; - var stackState = (StackLayoutState)context.LayoutState!; - double averageElementSize = GetAverageElementSize(availableSize, context, stackState) + Spacing; - - _orientation.SetMinorSize(ref extent, stackState.MaxArrangeBounds); - _orientation.SetMajorSize(ref extent, Math.Max(0.0f, itemsCount * averageElementSize - Spacing)); - if (itemsCount > 0) - { - if (firstRealized != null) - { - _orientation.SetMajorStart( - ref extent, - _orientation.MajorStart(firstRealizedLayoutBounds) - firstRealizedItemIndex * averageElementSize); - var remainingItems = itemsCount - lastRealizedItemIndex - 1; - _orientation.SetMajorSize( - ref extent, - _orientation.MajorEnd(lastRealizedLayoutBounds) - - _orientation.MajorStart(extent) + - (remainingItems * averageElementSize)); - } - else - { - Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", - LayoutId); - } - } - - Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on average {Average}", - LayoutId, extent.Size, averageElementSize); - return extent; - } - - internal void OnElementMeasured( - ILayoutable element, - int index, - Size availableSize, - Size measureSize, - Size desiredSize, - Size provisionalArrangeSize, - VirtualizingLayoutContext context) - { - if (context is VirtualizingLayoutContext virtualContext) - { - var stackState = (StackLayoutState)virtualContext.LayoutState!; - var provisionalArrangeSizeWinRt = provisionalArrangeSize; - stackState.OnElementMeasured( - index, - _orientation.Major(provisionalArrangeSizeWinRt), - _orientation.Minor(provisionalArrangeSizeWinRt)); - } - } - - Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize( - int index, - Size availableSize, - VirtualizingLayoutContext context) => availableSize; - - Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize( - int index, - Size measureSize, - Size desiredSize, - VirtualizingLayoutContext context) - { - var measureSizeMinor = _orientation.Minor(measureSize); - return _orientation.MinorMajorSize( - !double.IsInfinity(measureSizeMinor) ? - Math.Max(measureSizeMinor, _orientation.Minor(desiredSize)) : - _orientation.Minor(desiredSize), - _orientation.Major(desiredSize)); - } - - bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => true; - - FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect( - Size availableSize, - VirtualizingLayoutContext context) => GetAnchorForRealizationRect(availableSize, context); - - FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement( - int targetIndex, - Size availableSize, - VirtualizingLayoutContext context) - { - double offset = double.NaN; - int index = -1; - int itemsCount = context.ItemCount; - - if (targetIndex >= 0 && targetIndex < itemsCount) - { - index = targetIndex; - var state = (StackLayoutState)context.LayoutState!; - double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing; - offset = index * averageElementSize + _orientation.MajorStart(state.FlowAlgorithm.LastExtent); - } - - return new FlowLayoutAnchorInfo { Index = index, Offset = offset }; - } - - Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( - Size availableSize, - VirtualizingLayoutContext context, - ILayoutable? firstRealized, - int firstRealizedItemIndex, - Rect firstRealizedLayoutBounds, - ILayoutable? lastRealized, - int lastRealizedItemIndex, - Rect lastRealizedLayoutBounds) - { - return GetExtent( - availableSize, - context, - firstRealized, - firstRealizedItemIndex, - firstRealizedLayoutBounds, - lastRealized, - lastRealizedItemIndex, - lastRealizedLayoutBounds); - } - - void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) - { - OnElementMeasured( - element, - index, - availableSize, - measureSize, - desiredSize, - provisionalArrangeSize, - context); - } - - void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context) - { - } - - internal FlowLayoutAnchorInfo GetAnchorForRealizationRect( - Size availableSize, - VirtualizingLayoutContext context) - { - int anchorIndex = -1; - double offset = double.NaN; - - // Constants - int itemsCount = context.ItemCount; - if (itemsCount > 0) - { - var realizationRect = context.RealizationRect; - var state = (StackLayoutState)context.LayoutState!; - var lastExtent = state.FlowAlgorithm.LastExtent; - - double averageElementSize = GetAverageElementSize(availableSize, context, state) + Spacing; - double realizationWindowOffsetInExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); - double majorSize = _orientation.MajorSize(lastExtent) == 0 ? Math.Max(0.0, averageElementSize * itemsCount - Spacing) : _orientation.MajorSize(lastExtent); - if (itemsCount > 0 && - _orientation.MajorSize(realizationRect) >= 0 && - // MajorSize = 0 will account for when a nested repeater is outside the realization rect but still being measured. Also, - // note that if we are measuring this repeater, then we are already realizing an element to figure out the size, so we could - // just keep that element alive. It also helps in XYFocus scenarios to have an element realized for XYFocus to find a candidate - // in the navigating direction. - realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize) - { - anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize); - anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex)); - offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent); - } - } - - return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, }; - } - - protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) - { - var state = context.LayoutState; - var stackState = state as StackLayoutState; - - if (stackState == null) - { - if (state != null) - { - throw new InvalidOperationException("LayoutState must derive from StackLayoutState."); - } - - // Custom deriving layouts could potentially be stateful. - // If that is the case, we will just create the base state required by UniformGridLayout ourselves. - stackState = new StackLayoutState(); - } - - stackState.InitializeForContext(context, this); - } - - protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) - { - var stackState = (StackLayoutState)context.LayoutState!; - stackState.UninitializeForContext(context); - } - - protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) - { - ((StackLayoutState)context.LayoutState!).OnMeasureStart(); - - var desiredSize = GetFlowAlgorithm(context).Measure( - availableSize, - context, - false, - 0, - Spacing, - int.MaxValue, - _orientation.ScrollOrientation, - DisableVirtualization, - LayoutId); - - return new Size(desiredSize.Width, desiredSize.Height); - } - - protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) - { - var value = GetFlowAlgorithm(context).Arrange( - finalSize, - context, - false, - FlowLayoutAlgorithm.LineAlignment.Start, - LayoutId); - - return new Size(value.Width, value.Height); - } - - protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args) - { - GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context); - // Always invalidate layout to keep the view accurate. - InvalidateLayout(); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - if (change.Property == OrientationProperty) - { - var orientation = change.NewValue.GetValueOrDefault(); - - //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. - //Horizontal Orientation means we have a Horizontal ScrollOrientation. - _orientation.ScrollOrientation = orientation == Orientation.Horizontal ? ScrollOrientation.Horizontal : ScrollOrientation.Vertical; - } - - InvalidateLayout(); - } - - private double GetAverageElementSize( - Size availableSize, - VirtualizingLayoutContext context, - StackLayoutState stackLayoutState) - { - double averageElementSize = 0; - - if (context.ItemCount > 0) - { - if (stackLayoutState.TotalElementsMeasured == 0) - { - var tmpElement = context.GetOrCreateElementAt(0, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); - stackLayoutState.FlowAlgorithm.MeasureElement(tmpElement, 0, availableSize, context); - context.RecycleElement(tmpElement); - } - - averageElementSize = Math.Round(stackLayoutState.TotalElementSize / stackLayoutState.TotalElementsMeasured); - } - - return averageElementSize; - } - - private void InvalidateLayout() => InvalidateMeasure(); - - private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((StackLayoutState)context.LayoutState!).FlowAlgorithm; - } -} diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs deleted file mode 100644 index 3b82ece886..0000000000 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ /dev/null @@ -1,561 +0,0 @@ -// This source file is adapted from the WinUI project. -// (https://github.com/microsoft/microsoft-ui-xaml) -// -// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. - -using System; -using System.Collections.Specialized; -using Avalonia.Data; -using Avalonia.Logging; - -namespace Avalonia.Layout -{ - /// - /// Defines constants that specify how items are aligned on the non-scrolling or non-virtualizing axis. - /// - public enum UniformGridLayoutItemsJustification - { - /// - /// Items are aligned with the start of the row or column, with extra space at the end. - /// Spacing between items does not change. - /// - Start = 0, - - /// - /// Items are aligned in the center of the row or column, with extra space at the start and - /// end. Spacing between items does not change. - /// - Center = 1, - - /// - /// Items are aligned with the end of the row or column, with extra space at the start. - /// Spacing between items does not change. - /// - End = 2, - - /// - /// Items are aligned so that extra space is added evenly before and after each item. - /// - SpaceAround = 3, - - /// - /// Items are aligned so that extra space is added evenly between adjacent items. No space - /// is added at the start or end. - /// - SpaceBetween = 4, - - SpaceEvenly = 5, - }; - - /// - /// Defines constants that specify how items are sized to fill the available space. - /// - public enum UniformGridLayoutItemsStretch - { - /// - /// The item retains its natural size. Use of extra space is determined by the - /// property. - /// - None = 0, - - /// - /// The item is sized to fill the available space in the non-scrolling direction. Item size - /// in the scrolling direction is not changed. - /// - Fill = 1, - - /// - /// The item is sized to both fill the available space in the non-scrolling direction and - /// maintain its aspect ratio. - /// - Uniform = 2, - }; - - /// - /// Positions elements sequentially from left to right or top to bottom in a wrapping layout. - /// - public class UniformGridLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates - { - /// - /// Defines the property. - /// - public static readonly StyledProperty ItemsJustificationProperty = - AvaloniaProperty.Register(nameof(ItemsJustification)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ItemsStretchProperty = - AvaloniaProperty.Register(nameof(ItemsStretch)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinColumnSpacingProperty = - AvaloniaProperty.Register(nameof(MinColumnSpacing)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinItemHeightProperty = - AvaloniaProperty.Register(nameof(MinItemHeight)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinItemWidthProperty = - AvaloniaProperty.Register(nameof(MinItemWidth)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinRowSpacingProperty = - AvaloniaProperty.Register(nameof(MinRowSpacing)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MaximumRowsOrColumnsProperty = - AvaloniaProperty.Register(nameof(MinItemWidth)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty OrientationProperty = - StackLayout.OrientationProperty.AddOwner(); - - private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); - private double _minItemWidth = double.NaN; - private double _minItemHeight = double.NaN; - private double _minRowSpacing; - private double _minColumnSpacing; - private UniformGridLayoutItemsJustification _itemsJustification; - private UniformGridLayoutItemsStretch _itemsStretch; - private int _maximumRowsOrColumns = int.MaxValue; - - /// - /// Initializes a new instance of the class. - /// - public UniformGridLayout() - { - LayoutId = "UniformGridLayout"; - } - - static UniformGridLayout() - { - OrientationProperty.OverrideDefaultValue(Orientation.Horizontal); - } - - /// - /// Gets or sets a value that indicates how items are aligned on the non-scrolling or non- - /// virtualizing axis. - /// - /// - /// An enumeration value that indicates how items are aligned. The default is Start. - /// - public UniformGridLayoutItemsJustification ItemsJustification - { - get => GetValue(ItemsJustificationProperty); - set => SetValue(ItemsJustificationProperty, value); - } - - /// - /// Gets or sets a value that indicates how items are sized to fill the available space. - /// - /// - /// An enumeration value that indicates how items are sized to fill the available space. - /// The default is None. - /// - /// - /// This property enables adaptive layout behavior where the items are sized to fill the - /// available space along the non-scrolling axis, and optionally maintain their aspect ratio. - /// - public UniformGridLayoutItemsStretch ItemsStretch - { - get => GetValue(ItemsStretchProperty); - set => SetValue(ItemsStretchProperty, value); - } - - /// - /// Gets or sets the minimum space between items on the horizontal axis. - /// - /// - /// The spacing may exceed this minimum value when is set - /// to SpaceEvenly, SpaceAround, or SpaceBetween. - /// - public double MinColumnSpacing - { - get => GetValue(MinColumnSpacingProperty); - set => SetValue(MinColumnSpacingProperty, value); - } - - /// - /// Gets or sets the minimum height of each item. - /// - /// - /// The minimum height (in pixels) of each item. The default is NaN, in which case the - /// height of the first item is used as the minimum. - /// - public double MinItemHeight - { - get => GetValue(MinItemHeightProperty); - set => SetValue(MinItemHeightProperty, value); - } - - /// - /// Gets or sets the minimum width of each item. - /// - /// - /// The minimum width (in pixels) of each item. The default is NaN, in which case the width - /// of the first item is used as the minimum. - /// - public double MinItemWidth - { - get => GetValue(MinItemWidthProperty); - set => SetValue(MinItemWidthProperty, value); - } - - /// - /// Gets or sets the minimum space between items on the vertical axis. - /// - /// - /// The spacing may exceed this minimum value when is set - /// to SpaceEvenly, SpaceAround, or SpaceBetween. - /// - public double MinRowSpacing - { - get => GetValue(MinRowSpacingProperty); - set => SetValue(MinRowSpacingProperty, value); - } - - /// - /// Gets or sets the maximum row or column count. - /// - public int MaximumRowsOrColumns - { - get => GetValue(MaximumRowsOrColumnsProperty); - set => SetValue(MaximumRowsOrColumnsProperty, value); - } - - /// - /// Gets or sets the axis along which items are laid out. - /// - /// - /// One of the enumeration values that specifies the axis along which items are laid out. - /// The default is Vertical. - /// - public Orientation Orientation - { - get => GetValue(OrientationProperty); - set => SetValue(OrientationProperty, value); - } - - internal double LineSpacing => Orientation == Orientation.Horizontal ? _minRowSpacing : _minColumnSpacing; - internal double MinItemSpacing => Orientation == Orientation.Horizontal ? _minColumnSpacing : _minRowSpacing; - - Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize( - int index, - Size availableSize, - VirtualizingLayoutContext context) - { - var gridState = (UniformGridLayoutState)context.LayoutState!; - return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight); - } - - Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize( - int index, - Size measureSize, - Size desiredSize, - VirtualizingLayoutContext context) - { - var gridState = (UniformGridLayoutState)context.LayoutState!; - return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight); - } - - bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => remainingSpace < 0; - - FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect( - Size availableSize, - VirtualizingLayoutContext context) - { - Rect bounds = new Rect(double.NaN, double.NaN, double.NaN, double.NaN); - int anchorIndex = -1; - - int itemsCount = context.ItemCount; - var realizationRect = context.RealizationRect; - if (itemsCount > 0 && _orientation.MajorSize(realizationRect) > 0) - { - var gridState = (UniformGridLayoutState)context.LayoutState!; - var lastExtent = gridState.FlowAlgorithm.LastExtent; - var itemsPerLine = Math.Min( // note use of unsigned ints - Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), - Math.Max(1u, (uint)_maximumRowsOrColumns)); - var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); - var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); - if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize) - { - double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent)); - int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context)); - - anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); - bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context); - } - } - - return new FlowLayoutAnchorInfo - { - Index = anchorIndex, - Offset = _orientation.MajorStart(bounds) - }; - } - - FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement( - int targetIndex, - Size availableSize, - VirtualizingLayoutContext context) - { - int index = -1; - double offset = double.NaN; - int count = context.ItemCount; - if (targetIndex >= 0 && targetIndex < count) - { - int itemsPerLine = (int)Math.Min( // note use of unsigned ints - Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), - Math.Max(1u, _maximumRowsOrColumns)); - int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine; - index = indexOfFirstInLine; - var state = (UniformGridLayoutState)context.LayoutState!; - offset = _orientation.MajorStart(GetLayoutRectForDataIndex(availableSize, indexOfFirstInLine, state.FlowAlgorithm.LastExtent, context)); - } - - return new FlowLayoutAnchorInfo - { - Index = index, - Offset = offset - }; - } - - Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent( - Size availableSize, - VirtualizingLayoutContext context, - ILayoutable? firstRealized, - int firstRealizedItemIndex, - Rect firstRealizedLayoutBounds, - ILayoutable? lastRealized, - int lastRealizedItemIndex, - Rect lastRealizedLayoutBounds) - { - var extent = new Rect(); - - - // Constants - int itemsCount = context.ItemCount; - double availableSizeMinor = _orientation.Minor(availableSize); - int itemsPerLine = - (int)Math.Min( // note use of unsigned ints - Math.Max(1u, !double.IsInfinity(availableSizeMinor) - ? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context)) - : (uint)itemsCount), - Math.Max(1u, _maximumRowsOrColumns)); - double lineSize = GetMajorSizeWithSpacing(context); - - if (itemsCount > 0) - { - _orientation.SetMinorSize( - ref extent, - !double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ? - availableSizeMinor : - Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing)); - _orientation.SetMajorSize( - ref extent, - Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing)); - - if (firstRealized != null) - { - _orientation.SetMajorStart( - ref extent, - _orientation.MajorStart(firstRealizedLayoutBounds) - (firstRealizedItemIndex / itemsPerLine) * lineSize); - int remainingItems = itemsCount - lastRealizedItemIndex - 1; - _orientation.SetMajorSize( - ref extent, - _orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize); - } - else - { - Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", LayoutId); - } - } - - Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on lineSize {LineSize} and items per line {ItemsPerLine}", - LayoutId, extent.Size, lineSize, itemsPerLine); - return extent; - } - - void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context) - { - } - - void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context) - { - } - - protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) - { - var state = context.LayoutState; - var gridState = state as UniformGridLayoutState; - - if (gridState == null) - { - if (state != null) - { - throw new InvalidOperationException("LayoutState must derive from UniformGridLayoutState."); - } - - // Custom deriving layouts could potentially be stateful. - // If that is the case, we will just create the base state required by UniformGridLayout ourselves. - gridState = new UniformGridLayoutState(); - } - - gridState.InitializeForContext(context, this); - } - - protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) - { - var gridState = (UniformGridLayoutState)context.LayoutState!; - gridState.UninitializeForContext(context); - } - - protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) - { - // Set the width and height on the grid state. If the user already set them then use the preset. - // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items. - var gridState = (UniformGridLayoutState)context.LayoutState!; - gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns); - - var desiredSize = GetFlowAlgorithm(context).Measure( - availableSize, - context, - true, - MinItemSpacing, - LineSpacing, - _maximumRowsOrColumns, - _orientation.ScrollOrientation, - false, - LayoutId); - - // If after Measure the first item is in the realization rect, then we revoke grid state's ownership, - // and only use the layout when to clear it when it's done. - gridState.EnsureFirstElementOwnership(context); - - return desiredSize; - } - - protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) - { - var value = GetFlowAlgorithm(context).Arrange( - finalSize, - context, - true, - (FlowLayoutAlgorithm.LineAlignment)_itemsJustification, - LayoutId); - return new Size(value.Width, value.Height); - } - - protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args) - { - GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context); - // Always invalidate layout to keep the view accurate. - InvalidateLayout(); - - var gridState = (UniformGridLayoutState)context.LayoutState!; - gridState.ClearElementOnDataSourceChange(context, args); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - if (change.Property == OrientationProperty) - { - var orientation = change.NewValue.GetValueOrDefault(); - - //Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. - //i.e. the properties are the inverse of each other. - var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal; - _orientation.ScrollOrientation = scrollOrientation; - } - else if (change.Property == MinColumnSpacingProperty) - { - _minColumnSpacing = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == MinRowSpacingProperty) - { - _minRowSpacing = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == ItemsJustificationProperty) - { - _itemsJustification = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == ItemsStretchProperty) - { - _itemsStretch = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == MinItemWidthProperty) - { - _minItemWidth = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == MinItemHeightProperty) - { - _minItemHeight = change.NewValue.GetValueOrDefault(); - } - else if (change.Property == MaximumRowsOrColumnsProperty) - { - _maximumRowsOrColumns = change.NewValue.GetValueOrDefault(); - } - - InvalidateLayout(); - } - - private double GetMinorSizeWithSpacing(VirtualizingLayoutContext context) - { - var minItemSpacing = MinItemSpacing; - var gridState = (UniformGridLayoutState)context.LayoutState!; - return _orientation.ScrollOrientation == ScrollOrientation.Vertical? - gridState.EffectiveItemWidth + minItemSpacing : - gridState.EffectiveItemHeight + minItemSpacing; - } - - private double GetMajorSizeWithSpacing(VirtualizingLayoutContext context) - { - var lineSpacing = LineSpacing; - var gridState = (UniformGridLayoutState)context.LayoutState!; - return _orientation.ScrollOrientation == ScrollOrientation.Vertical ? - gridState.EffectiveItemHeight + lineSpacing : - gridState.EffectiveItemWidth + lineSpacing; - } - - Rect GetLayoutRectForDataIndex( - Size availableSize, - int index, - Rect lastExtent, - VirtualizingLayoutContext context) - { - int itemsPerLine = (int)Math.Min( //note use of unsigned ints - Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), - Math.Max(1u, _maximumRowsOrColumns)); - int rowIndex = (int)(index / itemsPerLine); - int indexInRow = index - (rowIndex * itemsPerLine); - - var gridState = (UniformGridLayoutState)context.LayoutState!; - Rect bounds = _orientation.MinorMajorRect( - indexInRow * GetMinorSizeWithSpacing(context) + _orientation.MinorStart(lastExtent), - rowIndex * GetMajorSizeWithSpacing(context) + _orientation.MajorStart(lastExtent), - _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight, - _orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth); - - return bounds; - } - - private void InvalidateLayout() => InvalidateMeasure(); - - private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((UniformGridLayoutState)context.LayoutState!).FlowAlgorithm; - } -} diff --git a/src/Avalonia.Layout/WrapLayout/WrapLayout.cs b/src/Avalonia.Layout/WrapLayout/WrapLayout.cs deleted file mode 100644 index aab0272f37..0000000000 --- a/src/Avalonia.Layout/WrapLayout/WrapLayout.cs +++ /dev/null @@ -1,336 +0,0 @@ -// 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. - -using Avalonia; -using Avalonia.Controls; -using Avalonia.Data; -using Avalonia.Layout; -using System; -using System.Collections.Specialized; - -namespace Avalonia.Layout -{ - /// - /// Arranges elements by wrapping them to fit the available space. - /// When is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row. - /// When is set to Orientation.Vertical, element are arranged in columns until the available height is reached. - /// - public class WrapLayout : VirtualizingLayout - { - /// - /// Gets or sets a uniform Horizontal distance (in pixels) between items when is set to Horizontal, - /// or between columns of items when is set to Vertical. - /// - public double HorizontalSpacing - { - get { return (double)GetValue(HorizontalSpacingProperty); } - set { SetValue(HorizontalSpacingProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly StyledProperty HorizontalSpacingProperty = - AvaloniaProperty.Register(nameof(HorizontalSpacing), 0); - - /// - /// Gets or sets a uniform Vertical distance (in pixels) between items when is set to Vertical, - /// or between rows of items when is set to Horizontal. - /// - public double VerticalSpacing - { - get { return (double)GetValue(VerticalSpacingProperty); } - set { SetValue(VerticalSpacingProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly StyledProperty VerticalSpacingProperty = - AvaloniaProperty.Register( - nameof(VerticalSpacing), 0d); - - /// - /// Gets or sets the orientation of the WrapLayout. - /// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls. - /// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added. - /// - public Orientation Orientation - { - get { return (Orientation)GetValue(OrientationProperty); } - set { SetValue(OrientationProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register( - nameof(Orientation), - Orientation.Horizontal); - - /// - protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) - { - var state = new WrapLayoutState(context); - context.LayoutState = state; - base.InitializeForContextCore(context); - } - - /// - protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) - { - context.LayoutState = null; - base.UninitializeForContextCore(context); - } - - /// - protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args) - { - var state = (WrapLayoutState)context.LayoutState!; - - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - state.RemoveFromIndex(args.NewStartingIndex); - break; - case NotifyCollectionChangedAction.Move: - int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex); - state.RemoveFromIndex(minIndex); - - state.RecycleElementAt(args.OldStartingIndex); - state.RecycleElementAt(args.NewStartingIndex); - break; - case NotifyCollectionChangedAction.Remove: - state.RemoveFromIndex(args.OldStartingIndex); - break; - case NotifyCollectionChangedAction.Replace: - state.RemoveFromIndex(args.NewStartingIndex); - state.RecycleElementAt(args.NewStartingIndex); - break; - case NotifyCollectionChangedAction.Reset: - state.Clear(); - break; - } - - base.OnItemsChangedCore(context, source, args); - } - - /// - protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) - { - var totalMeasure = UvMeasure.Zero; - var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height); - var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); - var realizationBounds = new UvBounds(Orientation, context.RealizationRect); - var position = UvMeasure.Zero; - - var state = (WrapLayoutState)context.LayoutState!; - if (state.Orientation != Orientation) - { - state.SetOrientation(Orientation); - } - - if (spacingMeasure.Equals(state.Spacing) == false) - { - state.ClearPositions(); - state.Spacing = spacingMeasure; - } - - if (state.AvailableU != parentMeasure.U) - { - state.ClearPositions(); - state.AvailableU = parentMeasure.U; - } - - double currentV = 0; - for (int i = 0; i < context.ItemCount; i++) - { - bool measured = false; - WrapItem item = state.GetItemAt(i); - if (item.Measure == null) - { - item.Element = context.GetOrCreateElementAt(i); - item.Element.Measure(availableSize); - item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); - measured = true; - } - - UvMeasure currentMeasure = item.Measure.Value; - if (currentMeasure.U == 0) - { - continue; // ignore collapsed items - } - - if (item.Position == null) - { - if (parentMeasure.U < position.U + currentMeasure.U) - { - // New Row - position.U = 0; - position.V += currentV + spacingMeasure.V; - currentV = 0; - } - - item.Position = position; - } - - position = item.Position.Value; - - double vEnd = position.V + currentMeasure.V; - if (vEnd < realizationBounds.VMin) - { - // Item is "above" the bounds - if (item.Element != null) - { - context.RecycleElement(item.Element); - item.Element = null; - } - } - else if (position.V > realizationBounds.VMax) - { - // Item is "below" the bounds. - if (item.Element != null) - { - context.RecycleElement(item.Element); - item.Element = null; - } - - // We don't need to measure anything below the bounds - break; - } - else if (measured == false) - { - // Always measure elements that are within the bounds - item.Element = context.GetOrCreateElementAt(i); - item.Element.Measure(availableSize); - - currentMeasure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); - if (currentMeasure.Equals(item.Measure) == false) - { - // this item changed size; we need to recalculate layout for everything after this - state.RemoveFromIndex(i + 1); - item.Measure = currentMeasure; - - // did the change make it go into the new row? - if (parentMeasure.U < position.U + currentMeasure.U) - { - // New Row - position.U = 0; - position.V += currentV + spacingMeasure.V; - currentV = 0; - } - - item.Position = position; - } - } - - position.U += currentMeasure.U + spacingMeasure.U; - currentV = Math.Max(currentMeasure.V, currentV); - } - - // update value with the last line - // if the the last loop is(parentMeasure.U > currentMeasure.U + lineMeasure.U) the total isn't calculated then calculate it - // if the last loop is (parentMeasure.U > currentMeasure.U) the currentMeasure isn't added to the total so add it here - // for the last condition it is zeros so adding it will make no difference - // this way is faster than an if condition in every loop for checking the last item - totalMeasure.U = parentMeasure.U; - - // Propagating an infinite size causes a crash. This can happen if the parent is scrollable and infinite in the opposite - // axis to the panel. Clearing to zero prevents the crash. - // This is likely an incorrect use of the control by the developer, however we need stability here so setting a default that wont crash. - if (double.IsInfinity(totalMeasure.U)) - { - totalMeasure.U = 0.0; - } - - totalMeasure.V = state.GetHeight(); - - totalMeasure.U = Math.Ceiling(totalMeasure.U); - - return Orientation == Orientation.Horizontal ? new Size(totalMeasure.U, totalMeasure.V) : new Size(totalMeasure.V, totalMeasure.U); - } - - /// - protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) - { - if (context.ItemCount > 0) - { - var parentMeasure = new UvMeasure(Orientation, finalSize.Width, finalSize.Height); - var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); - var realizationBounds = new UvBounds(Orientation, context.RealizationRect); - - var state = (WrapLayoutState)context.LayoutState!; - bool Arrange(WrapItem item, bool isLast = false) - { - if (item.Measure.HasValue == false) - { - return false; - } - - if (item.Position == null) - { - return false; - } - - var desiredMeasure = item.Measure.Value; - if (desiredMeasure.U == 0) - { - return true; // if an item is collapsed, avoid adding the spacing - } - - UvMeasure position = item.Position.Value; - - // Stretch the last item to fill the available space - if (isLast) - { - desiredMeasure.U = parentMeasure.U - position.U; - } - - if (((position.V + desiredMeasure.V) >= realizationBounds.VMin) && (position.V <= realizationBounds.VMax)) - { - // place the item - var child = context.GetOrCreateElementAt(item.Index); - if (Orientation == Orientation.Horizontal) - { - child.Arrange(new Rect(position.U, position.V, desiredMeasure.U, desiredMeasure.V)); - } - else - { - child.Arrange(new Rect(position.V, position.U, desiredMeasure.V, desiredMeasure.U)); - } - } - else if (position.V > realizationBounds.VMax) - { - return false; - } - - return true; - } - - for (var i = 0; i < context.ItemCount; i++) - { - bool continueArranging = Arrange(state.GetItemAt(i)); - if (continueArranging == false) - { - break; - } - } - } - - return finalSize; - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (change.Property == OrientationProperty || change.Property == HorizontalSpacingProperty || change.Property == VerticalSpacingProperty) - { - InvalidateMeasure(); - InvalidateArrange(); - } - } - } -} diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 70ab38e786..2001a2fcbc 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -28,4 +28,6 @@ + + diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index 2b32926008..76924d060f 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -8,6 +8,7 @@ - - + + + diff --git a/src/Avalonia.PlatformSupport/AssetLoader.cs b/src/Avalonia.PlatformSupport/AssetLoader.cs index fb03ec2f6e..0e33c3d4c7 100644 --- a/src/Avalonia.PlatformSupport/AssetLoader.cs +++ b/src/Avalonia.PlatformSupport/AssetLoader.cs @@ -14,14 +14,14 @@ namespace Avalonia.PlatformSupport /// public class AssetLoader : IAssetLoader { - private static AssemblyDescriptorResolver s_assemblyDescriptorResolver = new(); + private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver(); private AssemblyDescriptor? _defaultResmAssembly; /// /// Introduced for tests. /// - internal static void SetAssemblyDescriptorResolver(AssemblyDescriptorResolver resolver) => + internal static void SetAssemblyDescriptorResolver(IAssemblyDescriptorResolver resolver) => s_assemblyDescriptorResolver = resolver; /// @@ -182,13 +182,13 @@ namespace Avalonia.PlatformSupport throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri)); } - private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) + private (IAssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) { var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority); return (asm, uri.GetUnescapeAbsolutePath()); } - private AssemblyDescriptor? GetAssembly(Uri? uri) + private IAssemblyDescriptor? GetAssembly(Uri? uri) { if (uri != null) { diff --git a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj index 420ac0796c..5336f1e630 100644 --- a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj +++ b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj @@ -19,6 +19,6 @@ - + diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs index a3de7f2b8a..64ffec8482 100644 --- a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs @@ -6,7 +6,15 @@ using Avalonia.Utilities; namespace Avalonia.PlatformSupport.Internal; -internal class AssemblyDescriptor +internal interface IAssemblyDescriptor +{ + Assembly Assembly { get; } + Dictionary? Resources { get; } + Dictionary? AvaloniaResources { get; } + string? Name { get; } +} + +internal class AssemblyDescriptor : IAssemblyDescriptor { public AssemblyDescriptor(Assembly assembly) { diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs index a78051a9c4..28ae35d57d 100644 --- a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs @@ -5,11 +5,16 @@ using System.Reflection; namespace Avalonia.PlatformSupport.Internal; -internal class AssemblyDescriptorResolver +internal interface IAssemblyDescriptorResolver { - private readonly Dictionary _assemblyNameCache = new(); + IAssemblyDescriptor GetAssembly(string name); +} + +internal class AssemblyDescriptorResolver: IAssemblyDescriptorResolver +{ + private readonly Dictionary _assemblyNameCache = new(); - public AssemblyDescriptor GetAssembly(string name) + public IAssemblyDescriptor GetAssembly(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); diff --git a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj index 5a8301a2e9..d746c6db7e 100644 --- a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj +++ b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj @@ -7,7 +7,7 @@ Avalonia.Remote.Protocol - + \ No newline at end of file diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj new file mode 100644 index 0000000000..97e58f8a64 --- /dev/null +++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + enable + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/src/Avalonia.SourceGenerator/IsExternalInit.cs b/src/Avalonia.SourceGenerator/IsExternalInit.cs new file mode 100644 index 0000000000..c6ddf762ad --- /dev/null +++ b/src/Avalonia.SourceGenerator/IsExternalInit.cs @@ -0,0 +1,14 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal static class IsExternalInit + { + } +} diff --git a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs new file mode 100644 index 0000000000..4fc9397e7a --- /dev/null +++ b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Avalonia.SourceGenerator +{ + [Generator(LanguageNames.CSharp)] + public class SubtypesFactoryGenerator : IIncrementalGenerator + { + private record struct MethodTarget(IMethodSymbol Method, string MethodDecl, ITypeSymbol BaseType, string Namespace); + private static readonly string s_attributeName = typeof(SubtypesFactoryAttribute).FullName; + + private static bool IsSubtypeOf(ITypeSymbol type, ITypeSymbol baseType) + { + return type.BaseType is not null && (SymbolEqualityComparer.Default.Equals(type.BaseType, baseType) || IsSubtypeOf(type.BaseType, baseType)); + } + + private static void GenerateSubTypes(SourceProductionContext context, MethodTarget methodTarget, ImmutableArray types) + { + var (method, methodDecl, baseType, @namespace) = methodTarget; + var candidateTypes = types.Where(i => IsSubtypeOf(i, baseType)).Where(i => $"{i.ContainingNamespace}.".StartsWith($"{@namespace}.")).ToArray(); + var type = method.ContainingType; + var isGeneric = type.TypeParameters.Length > 0; + var isClass = type.TypeKind == TypeKind.Class; + + var typeDecl = $"partial {(isClass ? "class" : "struct")} {type.Name}{(isGeneric ? $"<{string.Join(", ", type.TypeParameters)}>" : "")}"; + var source = $@"using System; +using System.Collections.Generic; + +namespace {method.ContainingNamespace} +{{ + {typeDecl} + {{ + {methodDecl} + {{ + var hasMatch = false; + (hasMatch, {method.Parameters[1].Name}) = {method.Parameters[0].Name} switch + {{ +{string.Join("\n", candidateTypes.Select(i => $" \"{i.Name}\" => (true, ({method.Parameters[1].Type})new {i}()),"))} + _ => (false, default({method.Parameters[1].Type})) + }}; + + return hasMatch; + }} + }} +}}"; + + context.AddSource($"{type}.{method.MetadataName}.gen.cs", source); + } + + private static MethodTarget? PopulateMethodTargets(GeneratorSyntaxContext context, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + if (context.Node is MethodDeclarationSyntax method) + { + var attributes = method.AttributeLists.SelectMany(i => i.Attributes); + var semanticModel = context.SemanticModel; + foreach (var attribute in attributes) + { + var attributeTypeInfo = semanticModel.GetTypeInfo(attribute); + if (attributeTypeInfo.Type is null || + attributeTypeInfo.Type.ToString() != s_attributeName || + attribute.ArgumentList is null) + { + continue; + } + + var arguments = attribute.ArgumentList.Arguments; + if (arguments.Count != 2) + { + continue; + } + + if (arguments[0].Expression is not TypeOfExpressionSyntax typeOfExpr || + arguments[1].Expression is not LiteralExpressionSyntax and not IdentifierNameSyntax) + { + continue; + } + + var type = semanticModel.GetTypeInfo(typeOfExpr.Type); + var ns = semanticModel.GetConstantValue(arguments[1].Expression); + var methodDeclInfo = semanticModel.GetDeclaredSymbol(method); + + if (type.Type is not ITypeSymbol baseType || + ns.HasValue is false || + ns.Value is not string nsValue || + methodDeclInfo is not IMethodSymbol methodSymbol || + methodSymbol.Parameters.Length != 2 || + methodSymbol.Parameters[1].RefKind != RefKind.Out) + { + continue; + } + + var parameters = new SeparatedSyntaxList().AddRange(method.ParameterList.Parameters.Select(i => i.WithAttributeLists(new SyntaxList()))); + var methodDecl = method + .WithAttributeLists(new SyntaxList()) + .WithParameterList(method.ParameterList.WithParameters(parameters)) + .WithBody(null) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) + .WithoutTrivia().ToString(); + + return new MethodTarget(methodSymbol, methodDecl, baseType, nsValue); + } + } + + return null; + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var typesProvider = context.SyntaxProvider.CreateSyntaxProvider( + static (syntaxNode, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax; + }, + static (syntaxContext, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxContext.Node is ClassDeclarationSyntax or StructDeclarationSyntax && + syntaxContext.SemanticModel.GetDeclaredSymbol(syntaxContext.Node) is ITypeSymbol typeSymbol + ? typeSymbol : null; + }) + .SelectMany((type, token) => + { + token.ThrowIfCancellationRequested(); + return type is null ? Array.Empty() : new ITypeSymbol[] { type }; + }); + + var methodsProvider = context.SyntaxProvider.CreateSyntaxProvider( + static (syntaxNode, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxNode is MethodDeclarationSyntax { AttributeLists.Count: > 0 }; + }, PopulateMethodTargets) + .SelectMany((method, token) => + { + token.ThrowIfCancellationRequested(); + return method is null ? Array.Empty() : new MethodTarget[] { method.Value }; + }); + + var generateContext = methodsProvider.Combine(typesProvider.Collect()); + + context.RegisterSourceOutput(generateContext, static (sourceContext, source) => + { + sourceContext.CancellationToken.ThrowIfCancellationRequested(); + GenerateSubTypes(sourceContext, source.Left, source.Right); + }); + } + } +} diff --git a/src/Avalonia.Styling/ApiCompatBaseline.txt b/src/Avalonia.Styling/ApiCompatBaseline.txt deleted file mode 100644 index 0eedc3e360..0000000000 --- a/src/Avalonia.Styling/ApiCompatBaseline.txt +++ /dev/null @@ -1,4 +0,0 @@ -Compat issues with assembly Avalonia.Styling: -InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive' is present in the implementation but not in the contract. -InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive.get()' is present in the implementation but not in the contract. -Total Issues: 2 diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj deleted file mode 100644 index 139ba1bd2e..0000000000 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net6.0;netstandard2.0 - Avalonia.Styling - Avalonia - - - - - - - - diff --git a/src/Avalonia.Styling/Controls/ChildNameScope.cs b/src/Avalonia.Styling/Controls/ChildNameScope.cs deleted file mode 100644 index 58114a57fd..0000000000 --- a/src/Avalonia.Styling/Controls/ChildNameScope.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Threading.Tasks; -using Avalonia.Utilities; - -namespace Avalonia.Controls -{ - public class ChildNameScope : INameScope - { - private readonly INameScope _parentScope; - private readonly NameScope _inner = new NameScope(); - - public ChildNameScope(INameScope parentScope) - { - _parentScope = parentScope; - } - - public void Register(string name, object element) => _inner.Register(name, element); - - public SynchronousCompletionAsyncResult FindAsync(string name) - { - var found = Find(name); - if (found != null) - return new SynchronousCompletionAsyncResult(found); - // Not found and both current and parent scope are in completed state - if(IsCompleted) - return new SynchronousCompletionAsyncResult(null); - return DoFindAsync(name); - } - - public SynchronousCompletionAsyncResult DoFindAsync(string name) - { - var src = new SynchronousCompletionAsyncResultSource(); - - void ParentSearch() - { - var parentSearch = _parentScope.FindAsync(name); - if (parentSearch.IsCompleted) - src.SetResult(parentSearch.GetResult()); - else - parentSearch.OnCompleted(() => src.SetResult(parentSearch.GetResult())); - } - if (!_inner.IsCompleted) - { - // Guaranteed to be incomplete at this point - var innerSearch = _inner.FindAsync(name); - innerSearch.OnCompleted(() => - { - var value = innerSearch.GetResult(); - if (value != null) - src.SetResult(value); - else ParentSearch(); - }); - } - else - ParentSearch(); - - return src.AsyncResult; - } - - public object? Find(string name) - { - var found = _inner.Find(name); - if (found != null) - return found; - if (_inner.IsCompleted) - return _parentScope.Find(name); - return null; - } - - public void Complete() => _inner.Complete(); - - public bool IsCompleted => _inner.IsCompleted && _parentScope.IsCompleted; - } -} diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs deleted file mode 100644 index 77f98f85c4..0000000000 --- a/src/Avalonia.Styling/Controls/NameScope.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Avalonia.LogicalTree; -using Avalonia.Utilities; - -namespace Avalonia.Controls -{ - /// - /// Implements a name scope. - /// - public class NameScope : INameScope - { - /// - /// Defines the NameScope attached property. - /// - public static readonly AttachedProperty NameScopeProperty = - AvaloniaProperty.RegisterAttached("NameScope"); - - /// - public bool IsCompleted { get; private set; } - - private readonly Dictionary _inner = new Dictionary(); - - private readonly Dictionary> _pendingSearches = - new Dictionary>(); - - /// - /// Gets the value of the attached on a styled element. - /// - /// The styled element. - /// The value of the NameScope attached property. - public static INameScope GetNameScope(StyledElement styled) - { - _ = styled ?? throw new ArgumentNullException(nameof(styled)); - - return styled.GetValue(NameScopeProperty); - } - - /// - /// Sets the value of the attached on a styled element. - /// - /// The styled element. - /// The value to set. - public static void SetNameScope(StyledElement styled, INameScope value) - { - _ = styled ?? throw new ArgumentNullException(nameof(styled)); - - styled.SetValue(NameScopeProperty, value); - } - - /// - public void Register(string name, object element) - { - if (IsCompleted) - throw new InvalidOperationException("NameScope is completed, no further registrations are allowed"); - - _ = name ?? throw new ArgumentNullException(nameof(name)); - _ = element ?? throw new ArgumentNullException(nameof(element)); - - if (_inner.TryGetValue(name, out var existing)) - { - if (existing != element) - { - throw new ArgumentException($"Control with the name '{name}' already registered."); - } - } - else - { - _inner.Add(name, element); - if (_pendingSearches.TryGetValue(name, out var tcs)) - { - _pendingSearches.Remove(name); - tcs.SetResult(element); - } - } - } - - public SynchronousCompletionAsyncResult FindAsync(string name) - { - var found = Find(name); - if (found != null) - return new SynchronousCompletionAsyncResult(found); - if (IsCompleted) - return new SynchronousCompletionAsyncResult(null); - if (!_pendingSearches.TryGetValue(name, out var tcs)) - // We are intentionally running continuations synchronously here - _pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource(); - - return tcs.AsyncResult; - } - - /// - public object? Find(string name) - { - _ = name ?? throw new ArgumentNullException(nameof(name)); - - _inner.TryGetValue(name, out var result); - return result; - } - - public void Complete() - { - IsCompleted = true; - foreach (var kp in _pendingSearches) - kp.Value.TrySetResult(null); - _pendingSearches.Clear(); - } - - - } -} diff --git a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs deleted file mode 100644 index de41f5292c..0000000000 --- a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -#nullable enable -using System; - -namespace Avalonia.LogicalTree -{ - /// - /// Event args for event. - /// - public class ChildIndexChangedEventArgs : EventArgs - { - public ChildIndexChangedEventArgs() - { - } - - public ChildIndexChangedEventArgs(ILogical child) - { - Child = child; - } - - /// - /// Logical child which index was changed. - /// If null, all children should be reset. - /// - public ILogical? Child { get; } - } -} diff --git a/src/Avalonia.Styling/Properties/AssemblyInfo.cs b/src/Avalonia.Styling/Properties/AssemblyInfo.cs deleted file mode 100644 index ab034740ed..0000000000 --- a/src/Avalonia.Styling/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Runtime.CompilerServices; -using Avalonia.Metadata; - -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.LogicalTree")] -[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Styling")] - -[assembly: InternalsVisibleTo("Avalonia.Styling.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] - diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs deleted file mode 100644 index 5f498623e1..0000000000 --- a/src/Avalonia.Styling/StyledElement.cs +++ /dev/null @@ -1,888 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using Avalonia.Animation; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Data; -using Avalonia.Diagnostics; -using Avalonia.Logging; -using Avalonia.LogicalTree; -using Avalonia.Styling; - -#nullable enable - -namespace Avalonia -{ - /// - /// Extends an with the following features: - /// - /// - An inherited . - /// - Implements to allow styling to work on the styled element. - /// - Implements to form part of a logical tree. - /// - A collection of class strings for custom styling. - /// - public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent - { - /// - /// Defines the property. - /// - public static readonly StyledProperty DataContextProperty = - AvaloniaProperty.Register( - nameof(DataContext), - inherits: true, - notifying: DataContextNotifying); - - /// - /// Defines the property. - /// - public static readonly DirectProperty NameProperty = - AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); - - /// - /// Defines the property. - /// - public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); - - /// - /// Defines the property. - /// - public static readonly DirectProperty TemplatedParentProperty = - AvaloniaProperty.RegisterDirect( - nameof(TemplatedParent), - o => o.TemplatedParent, - (o ,v) => o.TemplatedParent = v); - - private int _initCount; - private string? _name; - private readonly Classes _classes = new Classes(); - private ILogicalRoot? _logicalRoot; - private IAvaloniaList? _logicalChildren; - private IResourceDictionary? _resources; - private Styles? _styles; - private bool _styled; - private List? _appliedStyles; - private ITemplatedControl? _templatedParent; - private bool _dataContextUpdating; - - /// - /// Initializes static members of the class. - /// - static StyledElement() - { - DataContextProperty.Changed.AddClassHandler((x,e) => x.OnDataContextChangedCore(e)); - } - - /// - /// Initializes a new instance of the class. - /// - public StyledElement() - { - _logicalRoot = this as ILogicalRoot; - } - - /// - /// Raised when the styled element is attached to a rooted logical tree. - /// - public event EventHandler? AttachedToLogicalTree; - - /// - /// Raised when the styled element is detached from a rooted logical tree. - /// - public event EventHandler? DetachedFromLogicalTree; - - /// - /// Occurs when the property changes. - /// - /// - /// This event will be raised when the property has changed and - /// all subscribers to that change have been notified. - /// - public event EventHandler? DataContextChanged; - - /// - /// Occurs when the styled element has finished initialization. - /// - /// - /// The Initialized event indicates that all property values on the styled element have been set. - /// When loading the styled element from markup, it occurs when - /// is called *and* the styled element - /// is attached to a rooted logical tree. When the styled element is created by code and - /// is not used, it is called when the styled element is attached - /// to the visual tree. - /// - public event EventHandler? Initialized; - - /// - /// Occurs when a resource in this styled element or a parent styled element has changed. - /// - public event EventHandler? ResourcesChanged; - - /// - /// Gets or sets the name of the styled element. - /// - /// - /// An element's name is used to uniquely identify an element within the element's name - /// scope. Once the element is added to a logical tree, its name cannot be changed. - /// - public string? Name - { - get => _name; - - set - { - if (_styled) - { - throw new InvalidOperationException("Cannot set Name : styled element already styled."); - } - - _name = value; - } - } - - /// - /// Gets or sets the styled element's classes. - /// - /// - /// - /// Classes can be used to apply user-defined styling to styled elements, or to allow styled elements - /// that share a common purpose to be easily selected. - /// - /// - /// Even though this property can be set, the setter is only intended for use in object - /// initializers. Assigning to this property does not change the underlying collection, - /// it simply clears the existing collection and adds the contents of the assigned - /// collection. - /// - /// - public Classes Classes - { - get - { - return _classes; - } - - set - { - if (_classes != value) - { - _classes.Replace(value); - } - } - } - - /// - /// Gets or sets the control's data context. - /// - /// - /// The data context is an inherited property that specifies the default object that will - /// be used for data binding. - /// - public object? DataContext - { - get { return GetValue(DataContextProperty); } - set { SetValue(DataContextProperty, value); } - } - - /// - /// Gets a value that indicates whether the element has finished initialization. - /// - /// - /// For more information about when IsInitialized is set, see the - /// event. - /// - public bool IsInitialized { get; private set; } - - /// - /// Gets the styles for the styled element. - /// - /// - /// Styles for the entire application are added to the Application.Styles collection, but - /// each styled element may in addition define its own styles which are applied to the styled element - /// itself and its children. - /// - public Styles Styles => _styles ??= new Styles(this); - - /// - /// Gets or sets the styled element's resource dictionary. - /// - public IResourceDictionary Resources - { - get => _resources ??= new ResourceDictionary(this); - set - { - value = value ?? throw new ArgumentNullException(nameof(value)); - _resources?.RemoveOwner(this); - _resources = value; - _resources.AddOwner(this); - } - } - - /// - /// Gets the styled element whose lookless template this styled element is part of. - /// - public ITemplatedControl? TemplatedParent - { - get => _templatedParent; - internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); - } - - /// - /// Gets the styled element's logical children. - /// - protected IAvaloniaList LogicalChildren - { - get - { - if (_logicalChildren == null) - { - var list = new AvaloniaList - { - ResetBehavior = ResetBehavior.Remove, - Validate = logical => ValidateLogicalChild(logical) - }; - list.CollectionChanged += LogicalChildrenCollectionChanged; - _logicalChildren = list; - } - - return _logicalChildren; - } - } - - /// - /// Gets the collection in a form that allows adding and removing - /// pseudoclasses. - /// - protected IPseudoClasses PseudoClasses => Classes; - - /// - /// Gets a value indicating whether the element is attached to a rooted logical tree. - /// - bool ILogical.IsAttachedToLogicalTree => _logicalRoot != null; - - /// - /// Gets the styled element's logical parent. - /// - public IStyledElement? Parent { get; private set; } - - /// - /// Gets the styled element's logical parent. - /// - ILogical? ILogical.LogicalParent => Parent; - - /// - /// Gets the styled element's logical children. - /// - IAvaloniaReadOnlyList ILogical.LogicalChildren => LogicalChildren; - - /// - bool IResourceNode.HasResources => (_resources?.HasResources ?? false) || - (((IResourceNode?)_styles)?.HasResources ?? false); - - /// - IAvaloniaReadOnlyList IStyleable.Classes => Classes; - - /// - /// Gets the type by which the styled element is styled. - /// - /// - /// Usually controls are styled by their own type, but there are instances where you want - /// a styled element to be styled by its base type, e.g. creating SpecialButton that - /// derives from Button and adds extra functionality but is still styled as a regular - /// Button. - /// - Type IStyleable.StyleKey => GetType(); - - /// - bool IStyleHost.IsStylesInitialized => _styles != null; - - /// - IStyleHost? IStyleHost.StylingParent => (IStyleHost?)InheritanceParent; - - /// - public virtual void BeginInit() - { - ++_initCount; - } - - /// - public virtual void EndInit() - { - if (_initCount == 0) - { - throw new InvalidOperationException("BeginInit was not called."); - } - - if (--_initCount == 0 && _logicalRoot != null) - { - ApplyStyling(); - InitializeIfNeeded(); - } - } - - /// - /// Applies styling to the control if the control is initialized and styling is not - /// already applied. - /// - /// - /// A value indicating whether styling is now applied to the control. - /// - protected bool ApplyStyling() - { - if (_initCount == 0 && !_styled) - { - try - { - BeginBatchUpdate(); - AvaloniaLocator.Current.GetService()?.ApplyStyles(this); - } - finally - { - EndBatchUpdate(); - } - - _styled = true; - } - - return _styled; - } - - /// - /// Detaches all styles from the element and queues a restyle. - /// - protected virtual void InvalidateStyles() => DetachStyles(); - - protected void InitializeIfNeeded() - { - if (_initCount == 0 && !IsInitialized) - { - IsInitialized = true; - OnInitialized(); - Initialized?.Invoke(this, EventArgs.Empty); - } - } - - internal StyleDiagnostics GetStyleDiagnosticsInternal() - { - IReadOnlyList? appliedStyles = _appliedStyles; - - if (appliedStyles is null) - { - appliedStyles = Array.Empty(); - } - - return new StyleDiagnostics(appliedStyles); - } - - /// - void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - OnAttachedToLogicalTreeCore(e); - } - - /// - void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - OnDetachedFromLogicalTreeCore(e); - } - - /// - void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); - - /// - void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); - - /// - bool IResourceNode.TryGetResource(object key, out object? value) - { - value = null; - return (_resources?.TryGetResource(key, out value) ?? false) || - (_styles?.TryGetResource(key, out value) ?? false); - } - - /// - /// Sets the styled element's logical parent. - /// - /// The parent. - void ISetLogicalParent.SetParent(ILogical? parent) - { - var old = Parent; - - if (parent != old) - { - if (old != null && parent != null) - { - throw new InvalidOperationException("The Control already has a parent."); - } - - if (InheritanceParent == null || parent == null) - { - InheritanceParent = parent as AvaloniaObject; - } - - Parent = (IStyledElement?)parent; - - if (_logicalRoot != null) - { - var e = new LogicalTreeAttachmentEventArgs(_logicalRoot, this, old!); - OnDetachedFromLogicalTreeCore(e); - } - - var newRoot = FindLogicalRoot(this); - - if (newRoot is object) - { - var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent!); - OnAttachedToLogicalTreeCore(e); - } - else if (parent is null) - { - // If we were attached to the logical tree, we piggyback on the tree traversal - // there to raise resources changed notifications. If we're being removed from - // the logical tree, then traverse the tree raising notifications now. - // - // We don't raise resources changed notifications if we're being attached to a - // non-rooted control beacuse it's unlikely that dynamic resources need to be - // correct until the control is added to the tree, and it causes a *lot* of - // notifications. - NotifyResourcesChanged(); - } - -#nullable disable - RaisePropertyChanged( - ParentProperty, - new Optional(old), - new BindingValue(Parent), - BindingPriority.LocalValue); -#nullable enable - } - } - - /// - /// Sets the styled element's inheritance parent. - /// - /// The parent. - void ISetInheritanceParent.SetParent(IAvaloniaObject? parent) - { - InheritanceParent = parent; - } - - void IStyleable.StyleApplied(IStyleInstance instance) - { - instance = instance ?? throw new ArgumentNullException(nameof(instance)); - - _appliedStyles ??= new List(); - _appliedStyles.Add(instance); - } - - void IStyleable.DetachStyles() => DetachStyles(); - - void IStyleable.DetachStyles(IReadOnlyList styles) => DetachStyles(styles); - - void IStyleable.InvalidateStyles() => InvalidateStyles(); - - void IStyleHost.StylesAdded(IReadOnlyList styles) - { - InvalidateStylesOnThisAndDescendents(); - } - - void IStyleHost.StylesRemoved(IReadOnlyList styles) - { - var allStyles = RecurseStyles(styles); - DetachStylesFromThisAndDescendents(allStyles); - } - - protected virtual void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - SetLogicalParent(e.NewItems!); - break; - - case NotifyCollectionChangedAction.Remove: - ClearLogicalParent(e.OldItems!); - break; - - case NotifyCollectionChangedAction.Replace: - ClearLogicalParent(e.OldItems!); - SetLogicalParent(e.NewItems!); - break; - - case NotifyCollectionChangedAction.Reset: - throw new NotSupportedException("Reset should not be signaled on LogicalChildren collection"); - } - } - - /// - /// Notifies child controls that a change has been made to resources that apply to them. - /// - /// The event args. - protected virtual void NotifyChildResourcesChanged(ResourcesChangedEventArgs e) - { - if (_logicalChildren is object) - { - var count = _logicalChildren.Count; - - if (count > 0) - { - e ??= ResourcesChangedEventArgs.Empty; - - for (var i = 0; i < count; ++i) - { - _logicalChildren[i].NotifyResourcesChanged(e); - } - } - } - } - - /// - /// Called when the styled element is added to a rooted logical tree. - /// - /// The event args. - protected virtual void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - } - - /// - /// Called when the styled element is removed from a rooted logical tree. - /// - /// The event args. - protected virtual void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - } - - /// - /// Called when the property changes. - /// - /// The event args. - protected virtual void OnDataContextChanged(EventArgs e) - { - DataContextChanged?.Invoke(this, EventArgs.Empty); - } - - /// - /// Called when the begins updating. - /// - protected virtual void OnDataContextBeginUpdate() - { - } - - /// - /// Called when the finishes updating. - /// - protected virtual void OnDataContextEndUpdate() - { - } - - /// - /// Called when the control finishes initialization. - /// - protected virtual void OnInitialized() - { - } - - private static void DataContextNotifying(IAvaloniaObject o, bool updateStarted) - { - if (o is StyledElement element) - { - DataContextNotifying(element, updateStarted); - } - } - - private static void DataContextNotifying(StyledElement element, bool updateStarted) - { - if (updateStarted) - { - if (!element._dataContextUpdating) - { - element._dataContextUpdating = true; - element.OnDataContextBeginUpdate(); - - var logicalChildren = element.LogicalChildren; - var logicalChildrenCount = logicalChildren.Count; - - for (var i = 0; i < logicalChildrenCount; i++) - { - if (element.LogicalChildren[i] is StyledElement s && - s.InheritanceParent == element && - !s.IsSet(DataContextProperty)) - { - DataContextNotifying(s, updateStarted); - } - } - } - } - else - { - if (element._dataContextUpdating) - { - element.OnDataContextEndUpdate(); - element._dataContextUpdating = false; - } - } - } - - private static ILogicalRoot? FindLogicalRoot(IStyleHost? e) - { - while (e != null) - { - if (e is ILogicalRoot root) - { - return root; - } - - e = e.StylingParent; - } - - return null; - } - - private static void ValidateLogicalChild(ILogical c) - { - if (c == null) - { - throw new ArgumentException("Cannot add null to LogicalChildren."); - } - } - - private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e) - { - if (this.GetLogicalParent() == null && !(this is ILogicalRoot)) - { - throw new InvalidOperationException( - $"AttachedToLogicalTreeCore called for '{GetType().Name}' but control has no logical parent."); - } - - // This method can be called when a control is already attached to the logical tree - // in the following scenario: - // - ListBox gets assigned Items containing ListBoxItem - // - ListBox makes ListBoxItem a logical child - // - ListBox template gets applied; making its Panel get attached to logical tree - // - That AttachedToLogicalTree signal travels down to the ListBoxItem - if (_logicalRoot == null) - { - _logicalRoot = e.Root; - - ApplyStyling(); - NotifyResourcesChanged(propagate: false); - - OnAttachedToLogicalTree(e); - AttachedToLogicalTree?.Invoke(this, e); - } - - var logicalChildren = LogicalChildren; - var logicalChildrenCount = logicalChildren.Count; - - for (var i = 0; i < logicalChildrenCount; i++) - { - if (logicalChildren[i] is StyledElement child) - { - child.OnAttachedToLogicalTreeCore(e); - } - } - } - - private void OnDetachedFromLogicalTreeCore(LogicalTreeAttachmentEventArgs e) - { - if (_logicalRoot != null) - { - _logicalRoot = null; - DetachStyles(); - OnDetachedFromLogicalTree(e); - DetachedFromLogicalTree?.Invoke(this, e); - - var logicalChildren = LogicalChildren; - var logicalChildrenCount = logicalChildren.Count; - - for (var i = 0; i < logicalChildrenCount; i++) - { - if (logicalChildren[i] is StyledElement child) - { - child.OnDetachedFromLogicalTreeCore(e); - } - } - -#if DEBUG - if (((INotifyCollectionChangedDebug)Classes).GetCollectionChangedSubscribers()?.Length > 0) - { - Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log( - this, - "{Type} detached from logical tree but still has class listeners", - GetType()); - } -#endif - } - } - - private void OnDataContextChangedCore(AvaloniaPropertyChangedEventArgs e) - { - OnDataContextChanged(EventArgs.Empty); - } - - private void SetLogicalParent(IList children) - { - var count = children.Count; - - for (var i = 0; i < count; i++) - { - var logical = (ILogical) children[i]!; - - if (logical.LogicalParent is null) - { - ((ISetLogicalParent)logical).SetParent(this); - } - } - } - - private void ClearLogicalParent(IList children) - { - var count = children.Count; - - for (var i = 0; i < count; i++) - { - var logical = (ILogical) children[i]!; - - if (logical.LogicalParent == this) - { - ((ISetLogicalParent)logical).SetParent(null); - } - } - } - - private void DetachStyles() - { - if (_appliedStyles is object) - { - BeginBatchUpdate(); - - try - { - foreach (var i in _appliedStyles) - { - i.Dispose(); - } - - _appliedStyles.Clear(); - } - finally - { - EndBatchUpdate(); - } - } - - _styled = false; - } - - private void DetachStyles(IReadOnlyList styles) - { - styles = styles ?? throw new ArgumentNullException(nameof(styles)); - - if (_appliedStyles is null) - { - return; - } - - var count = styles.Count; - - for (var i = 0; i < count; ++i) - { - for (var j = _appliedStyles.Count - 1; j >= 0; --j) - { - var applied = _appliedStyles[j]; - - if (applied.Source == styles[i]) - { - applied.Dispose(); - _appliedStyles.RemoveAt(j); - } - } - } - } - - private void InvalidateStylesOnThisAndDescendents() - { - InvalidateStyles(); - - if (_logicalChildren is object) - { - var childCount = _logicalChildren.Count; - - for (var i = 0; i < childCount; ++i) - { - (_logicalChildren[i] as StyledElement)?.InvalidateStylesOnThisAndDescendents(); - } - } - } - - private void DetachStylesFromThisAndDescendents(IReadOnlyList styles) - { - DetachStyles(styles); - - if (_logicalChildren is object) - { - var childCount = _logicalChildren.Count; - - for (var i = 0; i < childCount; ++i) - { - (_logicalChildren[i] as StyledElement)?.DetachStylesFromThisAndDescendents(styles); - } - } - } - - private void NotifyResourcesChanged( - ResourcesChangedEventArgs? e = null, - bool propagate = true) - { - if (ResourcesChanged is object) - { - e ??= ResourcesChangedEventArgs.Empty; - ResourcesChanged(this, e); - } - - if (propagate) - { - e ??= ResourcesChangedEventArgs.Empty; - NotifyChildResourcesChanged(e); - } - } - - private static IReadOnlyList RecurseStyles(IReadOnlyList styles) - { - var count = styles.Count; - List? result = null; - - for (var i = 0; i < count; ++i) - { - var style = styles[i]; - - if (style.Children.Count > 0) - { - if (result is null) - { - result = new List(styles); - } - - RecurseStyles(style.Children, result); - } - } - - return result ?? styles; - } - - private static void RecurseStyles(IReadOnlyList styles, List result) - { - var count = styles.Count; - - for (var i = 0; i < count; ++i) - { - var style = styles[i]; - result.Add(style); - RecurseStyles(style.Children, result); - } - } - } -} diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs deleted file mode 100644 index 168a882499..0000000000 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using Avalonia.Animation; -using Avalonia.Data; -using Avalonia.Data.Core; -using Avalonia.Metadata; -using Avalonia.Utilities; - -#nullable enable - -namespace Avalonia.Styling -{ - /// - /// A setter for a . - /// - /// - /// A is used to set a value on a - /// depending on a condition. - /// - public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor - { - private object? _value; - - /// - /// Initializes a new instance of the class. - /// - public Setter() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The property to set. - /// The property value. - public Setter(AvaloniaProperty property, object value) - { - Property = property; - Value = value; - } - - /// - /// Gets or sets the property to set. - /// - public AvaloniaProperty? Property { get; set; } - - /// - /// Gets or sets the property value. - /// - [Content] - [AssignBinding] - [DependsOn(nameof(Property))] - public object? Value - { - get => _value; - set - { - (value as ISetterValue)?.Initialize(this); - _value = value; - } - } - - public ISetterInstance Instance(IStyleable target) - { - target = target ?? throw new ArgumentNullException(nameof(target)); - - if (Property is null) - { - throw new InvalidOperationException("Setter.Property must be set."); - } - - var data = new SetterVisitorData - { - target = target, - value = Value, - }; - - Property.Accept(this, ref data); - return data.result!; - } - - void IAvaloniaPropertyVisitor.Visit( - StyledPropertyBase property, - ref SetterVisitorData data) - { - if (data.value is IBinding binding) - { - data.result = new PropertySetterBindingInstance( - data.target, - property, - binding); - } - else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType)) - { - data.result = new PropertySetterLazyInstance( - data.target, - property, - () => (T)template.Build()); - } - else - { - data.result = new PropertySetterInstance( - data.target, - property, - (T)data.value!); - } - } - - void IAvaloniaPropertyVisitor.Visit( - DirectPropertyBase property, - ref SetterVisitorData data) - { - if (data.value is IBinding binding) - { - data.result = new PropertySetterBindingInstance( - data.target, - property, - binding); - } - else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType)) - { - data.result = new PropertySetterLazyInstance( - data.target, - property, - () => (T)template.Build()); - } - else - { - data.result = new PropertySetterInstance( - data.target, - property, - (T)data.value!); - } - } - - private struct SetterVisitorData - { - public IStyleable target; - public object? value; - public ISetterInstance? result; - } - } -} diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index ef200b5532..40ed4a0f87 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -3,16 +3,10 @@ net6.0;netstandard2.0 - - - - - - - + diff --git a/src/Avalonia.Themes.Default/Controls/CheckBox.xaml b/src/Avalonia.Themes.Default/Controls/CheckBox.xaml index fe226164a5..170ecdd5b8 100644 --- a/src/Avalonia.Themes.Default/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Default/Controls/CheckBox.xaml @@ -26,6 +26,7 @@ Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center" + FlowDirection="LeftToRight" Data="M 1145.607177734375,430 C1145.607177734375,430 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1138,434.5538330078125 1138,434.5538330078125 1138,434.5538330078125 1141.482177734375,438 1141.482177734375,438 1141.482177734375,438 1141.96875,437.9375 1141.96875,437.9375 1141.96875,437.9375 1147,431.34619140625 1147,431.34619140625 1147,431.34619140625 1145.607177734375,430 1145.607177734375,430 z"/> + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml index b357446bfa..aab1b76259 100644 --- a/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml @@ -13,7 +13,7 @@ CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" Content="{TemplateBinding Content}" Extent="{TemplateBinding Extent, Mode=TwoWay}" - Margin="{TemplateBinding Padding}" + Padding="{TemplateBinding Padding}" Offset="{TemplateBinding Offset, Mode=TwoWay}" Viewport="{TemplateBinding Viewport, Mode=TwoWay}" IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"> diff --git a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml index 2d65ea2b7b..0c46ce3724 100644 --- a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml @@ -16,6 +16,7 @@ 24 24 1 + 24 1 @@ -59,8 +60,11 @@ + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 846d45b839..468b723f5b 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -14,6 +14,7 @@ + diff --git a/src/Avalonia.Themes.Default/SimpleTheme.cs b/src/Avalonia.Themes.Default/SimpleTheme.cs index 1d9f2d5f9d..6929660757 100644 --- a/src/Avalonia.Themes.Default/SimpleTheme.cs +++ b/src/Avalonia.Themes.Default/SimpleTheme.cs @@ -116,7 +116,7 @@ namespace Avalonia.Themes.Default return false; } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ModeProperty) diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 885661536c..35603fe216 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -3,16 +3,10 @@ net6.0;netstandard2.0 - - - - - - - + diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 7a0f9af83d..f545206a2f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -4,7 +4,7 @@