Browse Source

Merge remote-tracking branch 'upstream/master' into feature/InlineUIContainer

pull/7946/head
Benedikt Stebner 4 years ago
parent
commit
36cc39ceb6
  1. 49
      Avalonia.sln
  2. 2
      build/DevAnalyzers.props
  3. 10
      build/SourceGenerators.props
  4. 5
      native/Avalonia.Native/src/OSX/app.mm
  5. 2
      native/Avalonia.Native/src/OSX/window.mm
  6. 1
      nukebuild/Build.cs
  7. 44
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  8. 5
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  9. 7
      samples/ControlCatalog.NetCore/rd.xml
  10. 53
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  11. 19
      samples/ControlCatalog/Pages/DataGridPage.xaml
  12. 2
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  13. 10
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  14. 13
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  15. 5
      samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
  16. 79
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  17. 104
      src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
  18. 4
      src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
  19. 50
      src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt
  20. BIN
      src/Android/Avalonia.AndroidTestApplication/Resources/drawable/Icon.png
  21. 6
      src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml
  22. 7
      src/Avalonia.Base/Animation/Animatable.cs
  23. 40
      src/Avalonia.Base/Animation/Easings/Easing.cs
  24. 3
      src/Avalonia.Base/ApiCompatBaseline.txt
  25. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  26. 77
      src/Avalonia.Base/AvaloniaObject.cs
  27. 234
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  28. 23
      src/Avalonia.Base/AvaloniaProperty.cs
  29. 43
      src/Avalonia.Base/AvaloniaPropertyChangedExtensions.cs
  30. 44
      src/Avalonia.Base/DirectPropertyBase.cs
  31. 2
      src/Avalonia.Base/GeometryGroup.cs
  32. 104
      src/Avalonia.Base/IAvaloniaObject.cs
  33. 3
      src/Avalonia.Base/Input/IInputRoot.cs
  34. 6
      src/Avalonia.Base/Input/IKeyboardDevice.cs
  35. 2
      src/Avalonia.Base/Input/IMouseDevice.cs
  36. 16
      src/Avalonia.Base/Input/IPointerDevice.cs
  37. 8
      src/Avalonia.Base/Input/InputElement.cs
  38. 5
      src/Avalonia.Base/Input/KeyGesture.cs
  39. 2
      src/Avalonia.Base/Input/KeyboardDevice.cs
  40. 2
      src/Avalonia.Base/Input/KeyboardNavigation.cs
  41. 328
      src/Avalonia.Base/Input/MouseDevice.cs
  42. 5
      src/Avalonia.Base/Input/Navigation/TabNavigation.cs
  43. 2
      src/Avalonia.Base/Input/PointerEventArgs.cs
  44. 209
      src/Avalonia.Base/Input/PointerOverPreProcessor.cs
  45. 2
      src/Avalonia.Base/Input/Raw/RawDragEvent.cs
  46. 27
      src/Avalonia.Base/Input/Raw/RawInputHelpers.cs
  47. 2
      src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
  48. 47
      src/Avalonia.Base/Input/TouchDevice.cs
  49. 24
      src/Avalonia.Base/Interactivity/IInteractive.cs
  50. 4
      src/Avalonia.Base/Layout/StackLayout.cs
  51. 18
      src/Avalonia.Base/Layout/UniformGridLayout.cs
  52. 2
      src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs
  53. 4
      src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
  54. 83
      src/Avalonia.Base/Media/Color.cs
  55. 5
      src/Avalonia.Base/Media/DashStyle.cs
  56. 2
      src/Avalonia.Base/Media/DrawingImage.cs
  57. 55
      src/Avalonia.Base/Media/HslColor.cs
  58. 55
      src/Avalonia.Base/Media/HsvColor.cs
  59. 28
      src/Avalonia.Base/Media/Pen.cs
  60. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  61. 17
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  62. 13
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  63. 2
      src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
  64. 3
      src/Avalonia.Base/PropertyStore/IValue.cs
  65. 17
      src/Avalonia.Base/PropertyStore/IValueSink.cs
  66. 5
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  67. 67
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  68. 45
      src/Avalonia.Base/PropertyStore/ValueOwner.cs
  69. 2
      src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs
  70. 30
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  71. 15
      src/Avalonia.Base/RelativePoint.cs
  72. 7
      src/Avalonia.Base/StyledElement.cs
  73. 44
      src/Avalonia.Base/StyledPropertyBase.cs
  74. 65
      src/Avalonia.Base/Styling/Setter.cs
  75. 2
      src/Avalonia.Base/Threading/AvaloniaScheduler.cs
  76. 12
      src/Avalonia.Base/Threading/Dispatcher.cs
  77. 97
      src/Avalonia.Base/Threading/DispatcherPriority.cs
  78. 4
      src/Avalonia.Base/Threading/DispatcherTimer.cs
  79. 32
      src/Avalonia.Base/Threading/IDispatcher.cs
  80. 19
      src/Avalonia.Base/Utilities/EnumHelper.cs
  81. 32
      src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs
  82. 29
      src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs
  83. 138
      src/Avalonia.Base/Utilities/WeakEvent.cs
  84. 241
      src/Avalonia.Base/Utilities/WeakHashList.cs
  85. 27
      src/Avalonia.Base/ValueStore.cs
  86. 21
      src/Avalonia.Base/Visual.cs
  87. 3
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  88. 10
      src/Avalonia.Build.Tasks/Properties/launchSettings.json
  89. 5
      src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt
  90. 18
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  91. 1
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  92. 2
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  93. 4
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  94. 36
      src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs
  95. 9
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  96. 36
      src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
  97. 46
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  98. 6
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  99. 1
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  100. 7
      src/Avalonia.Controls.DataGrid/DataGridRows.cs

49
Avalonia.sln

@ -39,6 +39,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}"
@ -55,8 +56,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Android", "src\Android\Avalonia.Android\Avalonia.Android.csproj", "{7B92AF71-6287-4693-9DCB-BD5B6E927E23}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Android", "src\Android\Avalonia.Android\Avalonia.Android.csproj", "{7B92AF71-6287-4693-9DCB-BD5B6E927E23}"
EndProject 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "iOS", "iOS", "{0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.iOS", "src\iOS\Avalonia.iOS\Avalonia.iOS.csproj", "{4488AD85-1495-4809-9AA4-DDFE0A48527E}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.iOS", "src\iOS\Avalonia.iOS\Avalonia.iOS.csproj", "{4488AD85-1495-4809-9AA4-DDFE0A48527E}"
@ -113,6 +112,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\SharedVersion.props = build\SharedVersion.props build\SharedVersion.props = build\SharedVersion.props
build\SharpDX.props = build\SharpDX.props build\SharpDX.props = build\SharpDX.props
build\SkiaSharp.props = build\SkiaSharp.props build\SkiaSharp.props = build\SkiaSharp.props
build\SourceGenerators.props = build\SourceGenerators.props
build\SourceLink.props = build\SourceLink.props build\SourceLink.props = build\SourceLink.props
build\System.Drawing.Common.props = build\System.Drawing.Common.props build\System.Drawing.Common.props = build\System.Drawing.Common.props
build\System.Memory.props = build\System.Memory.props build\System.Memory.props = build\System.Memory.props
@ -209,10 +209,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}"
EndProject 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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}"
EndProject EndProject
Global Global
@ -629,22 +631,6 @@ Global
{7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|Any CPU.Build.0 = Release|Any CPU {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|iPhone.ActiveCfg = Release|Any CPU
{7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|iPhoneSimulator.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.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|Any CPU.Build.0 = Release|Any CPU
{4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
@ -1929,6 +1915,30 @@ Global
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.Build.0 = Release|Any CPU {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.ActiveCfg = Release|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.Build.0 = 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.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|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.ActiveCfg = Debug|Any CPU
@ -1970,7 +1980,6 @@ Global
{8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098} {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} {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} {4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}
{E1AA3DBF-9056-4530-9376-18119A7A3FFE} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {E1AA3DBF-9056-4530-9376-18119A7A3FFE} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{88060192-33D5-4932-B0F9-8BD2763E857D} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {88060192-33D5-4932-B0F9-8BD2763E857D} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}

2
build/DevAnalyzers.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)..\src\Tools\DevAnalyzers\DevAnalyzers.csproj" <ProjectReference Include="$(MSBuildThisFileDirectory)..\src\tools\DevAnalyzers\DevAnalyzers.csproj"
PrivateAssets="all" PrivateAssets="all"
ReferenceOutputAssembly="false" ReferenceOutputAssembly="false"
OutputItemType="Analyzer" OutputItemType="Analyzer"

10
build/SourceGenerators.props

@ -0,0 +1,10 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ProjectReference
Include="$(MSBuildThisFileDirectory)/../src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
PrivateAssets="all" />
<Compile Include="$(MSBuildThisFileDirectory)/../src/Shared/SourceGeneratorAttributes.cs" />
</ItemGroup>
</Project>

5
native/Avalonia.Native/src/OSX/app.mm

@ -73,6 +73,11 @@ ComPtr<IAvnApplicationEvents> _events;
_isHandlingSendEvent = true; _isHandlingSendEvent = true;
@try { @try {
[super sendEvent: event]; [super sendEvent: event];
if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & NSEventModifierFlagCommand))
{
[[self keyWindow] sendEvent:event];
}
} @finally { } @finally {
_isHandlingSendEvent = oldHandling; _isHandlingSendEvent = oldHandling;
} }

2
native/Avalonia.Native/src/OSX/window.mm

@ -457,7 +457,7 @@ public:
} }
point = ConvertPointY(point); 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); auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y);
*ret = [View translateLocalPoint:ToAvnPoint(viewPoint)]; *ret = [View translateLocalPoint:ToAvnPoint(viewPoint)];

1
nukebuild/Build.cs

@ -221,6 +221,7 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); RunCoreTest("Avalonia.Markup.Xaml.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests");
RunCoreTest("Avalonia.PlatformSupport.UnitTests");
}); });
Target RunRenderTests => _ => _ Target RunRenderTests => _ => _

44
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -39,49 +39,9 @@
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" /> <PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.3.1.3" /> <PackageReference Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.3.1.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj">
<Project>{7b92af71-6287-4693-9dcb-bd5b6e927e23}</Project>
<Name>Avalonia.Android</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj">
<Project>{b09b78d8-9b26-48b0-9149-d64a2f120f3f}</Project>
<Name>Avalonia.Base</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj">
<Project>{d2221c82-4a25-4583-9b43-d791e3f6820c}</Project>
<Name>Avalonia.Controls</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj">
<Project>{7062ae20-5dcc-4442-9645-8195bdece63e}</Project>
<Name>Avalonia.Diagnostics</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj">
<Project>{3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f}</Project>
<Name>Avalonia.Themes.Default</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj">
<Project>{3e53a01a-b331-47f3-b828-4a5717e77a24}</Project>
<Name>Avalonia.Markup.Xaml</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj">
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
<Name>Avalonia.Markup</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj">
<Project>{7d2d3083-71dd-4cc9-8907-39a0d86fb322}</Project>
<Name>Avalonia.Skia</Name>
</ProjectReference>
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj">
<Project>{d0a739b9-3c68-4ba6-a328-41606954b6bd}</Project>
<Name>ControlCatalog</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" /> <ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\Rx.props" /> </Project>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<Import Project="..\..\build\AndroidWorkarounds.props" />
<Import Project="..\..\build\LegacyProject.targets" />
</Project>

5
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -6,7 +6,7 @@
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch> <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(RunAotCompilation)' == 'true'"> <PropertyGroup Condition="'$(RunNativeAotCompilation)' == 'true'">
<IlcTrimMetadata>true</IlcTrimMetadata> <IlcTrimMetadata>true</IlcTrimMetadata>
<RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json</RestoreAdditionalProjectSources> <RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json</RestoreAdditionalProjectSources>
<NativeAotCompilerVersion>7.0.0-*</NativeAotCompilerVersion> <NativeAotCompilerVersion>7.0.0-*</NativeAotCompilerVersion>
@ -22,12 +22,11 @@
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" /> <PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(RunAotCompilation)' == 'true'"> <ItemGroup Condition="'$(RunNativeAotCompilation)' == 'true'">
<PackageReference Include="Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" /> <PackageReference Include="Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" />
<!-- Cross-compilation for Windows x64-arm64 and Linux x64-arm64 --> <!-- Cross-compilation for Windows x64-arm64 and Linux x64-arm64 -->
<PackageReference Condition="'$(RuntimeIdentifier)'=='win-arm64'" Include="runtime.win-x64.Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" /> <PackageReference Condition="'$(RuntimeIdentifier)'=='win-arm64'" Include="runtime.win-x64.Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" />
<PackageReference Condition="'$(RuntimeIdentifier)'=='linux-arm64'" Include="runtime.linux-x64.Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" /> <PackageReference Condition="'$(RuntimeIdentifier)'=='linux-arm64'" Include="runtime.linux-x64.Microsoft.DotNet.ILCompiler" Version="$(NativeAotCompilerVersion)" />
<RdXmlFile Include="rd.xml" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>

7
samples/ControlCatalog.NetCore/rd.xml

@ -1,7 +0,0 @@
<Directives>
<Application>
<Assembly Name="ControlCatalog" Dynamic="Required All"></Assembly>
<Assembly Name="Avalonia.Themes.Default" Dynamic="Required All"></Assembly>
<Assembly Name="Avalonia.Themes.Fluent" Dynamic="Required All"></Assembly>
</Application>
</Directives>

53
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@ -7,59 +7,10 @@
<!-- temporal workaround for our GL interface backend --> <!-- temporal workaround for our GL interface backend -->
<UseInterpreter>True</UseInterpreter> <UseInterpreter>True</UseInterpreter>
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
<!-- <RuntimeIdentifier>ios-arm64</RuntimeIdentifier>--> <!-- <RuntimeIdentifier>ios-arm64</RuntimeIdentifier>-->
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" /> <ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> </Project>
<Reference Include="System" />
<Reference Include="System.Xml" />
<Reference Include="System.Core" />
<Reference Include="Xamarin.iOS" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj">
<Project>{4488ad85-1495-4809-9aa4-ddfe0a48527e}</Project>
<Name>Avalonia.iOS</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj">
<Project>{3E53A01A-B331-47F3-B828-4A5717E77A24}</Project>
<Name>Avalonia.Markup.Xaml</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj">
<Project>{6417E941-21BC-467B-A771-0DE389353CE6}</Project>
<Name>Avalonia.Markup</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj">
<Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
<Name>Avalonia.Base</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj">
<Project>{D2221C82-4A25-4583-9B43-D791E3F6820C}</Project>
<Name>Avalonia.Controls</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj">
<Project>{7062AE20-5DCC-4442-9645-8195BDECE63E}</Project>
<Name>Avalonia.Diagnostics</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj">
<Project>{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}</Project>
<Name>Avalonia.Themes.Default</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj">
<Project>{7d2d3083-71dd-4cc9-8907-39a0d86fb322}</Project>
<Name>Avalonia.Skia</Name>
</ProjectReference>
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj">
<Project>{d0a739b9-3c68-4ba6-a328-41606954b6bd}</Project>
<Name>ControlCatalog</Name>
</ProjectReference>
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.2-preview.33" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
<Import Project="..\..\build\LegacyProject.targets" />
<Import Project="..\..\build\SkiaSharp.props" />
<Import Project="..\..\build\HarfBuzzSharp.props" />
</Project>

19
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -13,9 +13,22 @@
</UserControl.Resources> </UserControl.Resources>
<UserControl.Styles> <UserControl.Styles>
<Style Selector="DataGridCell.gdp"> <Style Selector="DataGridCell.gdp">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Background" Value="{Binding Path=GDP, Mode=OneWay, Converter={StaticResource GDPConverter}}" /> <Setter Property="Background" Value="{Binding Path=GDP, Mode=OneWay, Converter={StaticResource GDPConverter}}" />
</Style> </Style>
<Style Selector="DataGridColumnHeader:nth-last-child(1)">
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="DataGridCell:nth-last-child(1)">
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="DataGrid#dataGridGrouping DataGridRow:nth-child(5n+3)">
<Setter Property="Foreground" Value="Red" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="DataGrid#dataGridGrouping DataGridRow:nth-last-child(5n+1)">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</UserControl.Styles> </UserControl.Styles>
<Grid RowDefinitions="Auto,Auto,*"> <Grid RowDefinitions="Auto,Auto,*">
<StackPanel Orientation="Vertical" Spacing="4" Grid.Row="0"> <StackPanel Orientation="Vertical" Spacing="4" Grid.Row="0">
@ -31,7 +44,9 @@
<DockPanel> <DockPanel>
<CheckBox x:Name="ShowGDP" IsChecked="True" Content="Toggle GDP Column Visibility" <CheckBox x:Name="ShowGDP" IsChecked="True" Content="Toggle GDP Column Visibility"
DockPanel.Dock="Top"/> DockPanel.Dock="Top"/>
<DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All"> <DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All"
RowBackground="#1000"
AlternatingRowBackground="#1fff">
<DataGrid.Columns> <DataGrid.Columns>
<!-- Using HeaderTemplate --> <!-- Using HeaderTemplate -->
<DataGridTextColumn Header="Country" HeaderTemplate="{StaticResource Demo.DataTemplates.CountryHeader}" Binding="{Binding Name}" Width="6*" /> <DataGridTextColumn Header="Country" HeaderTemplate="{StaticResource Demo.DataTemplates.CountryHeader}" Binding="{Binding Name}" Width="6*" />

2
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -53,10 +53,12 @@
<ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem> <ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem>
</ComboBox> </ComboBox>
<Button Command="{Binding AddItem}">Add Item</Button> <Button Command="{Binding AddItem}">Add Item</Button>
<Button Command="{Binding RemoveItem}">Remove Item</Button>
<Button Command="{Binding RandomizeHeights}">Randomize Heights</Button> <Button Command="{Binding RandomizeHeights}">Randomize Heights</Button>
<Button Command="{Binding ResetItems}">Reset items</Button> <Button Command="{Binding ResetItems}">Reset items</Button>
<Button x:Name="scrollToLast">Scroll to Last</Button> <Button x:Name="scrollToLast">Scroll to Last</Button>
<Button x:Name="scrollToRandom">Scroll to Random</Button> <Button x:Name="scrollToRandom">Scroll to Random</Button>
<Button x:Name="scrollToSelected">Scroll to Selected</Button>
</StackPanel> </StackPanel>
<Border BorderThickness="1" BorderBrush="{DynamicResource SystemControlHighlightBaseMediumLowBrush}" Margin="0 0 0 16"> <Border BorderThickness="1" BorderBrush="{DynamicResource SystemControlHighlightBaseMediumLowBrush}" Margin="0 0 0 16">
<ScrollViewer Name="scroller" <ScrollViewer Name="scroller"

10
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@ -15,8 +15,10 @@ namespace ControlCatalog.Pages
private readonly ItemsRepeaterPageViewModel _viewModel; private readonly ItemsRepeaterPageViewModel _viewModel;
private ItemsRepeater _repeater; private ItemsRepeater _repeater;
private ScrollViewer _scroller; private ScrollViewer _scroller;
private int _selectedIndex;
private Button _scrollToLast; private Button _scrollToLast;
private Button _scrollToRandom; private Button _scrollToRandom;
private Button _scrollToSelected;
private Random _random = new Random(0); private Random _random = new Random(0);
public ItemsRepeaterPage() public ItemsRepeaterPage()
@ -26,10 +28,12 @@ namespace ControlCatalog.Pages
_scroller = this.FindControl<ScrollViewer>("scroller"); _scroller = this.FindControl<ScrollViewer>("scroller");
_scrollToLast = this.FindControl<Button>("scrollToLast"); _scrollToLast = this.FindControl<Button>("scrollToLast");
_scrollToRandom = this.FindControl<Button>("scrollToRandom"); _scrollToRandom = this.FindControl<Button>("scrollToRandom");
_scrollToSelected = this.FindControl<Button>("scrollToSelected");
_repeater.PointerPressed += RepeaterClick; _repeater.PointerPressed += RepeaterClick;
_repeater.KeyDown += RepeaterOnKeyDown; _repeater.KeyDown += RepeaterOnKeyDown;
_scrollToLast.Click += scrollToLast_Click; _scrollToLast.Click += scrollToLast_Click;
_scrollToRandom.Click += scrollToRandom_Click; _scrollToRandom.Click += scrollToRandom_Click;
_scrollToSelected.Click += scrollToSelected_Click;
DataContext = _viewModel = new ItemsRepeaterPageViewModel(); DataContext = _viewModel = new ItemsRepeaterPageViewModel();
} }
@ -121,6 +125,7 @@ namespace ControlCatalog.Pages
{ {
var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item; var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item;
_viewModel.SelectedItem = item; _viewModel.SelectedItem = item;
_selectedIndex = _viewModel.Items.IndexOf(item);
} }
private void RepeaterOnKeyDown(object sender, KeyEventArgs e) private void RepeaterOnKeyDown(object sender, KeyEventArgs e)
@ -140,5 +145,10 @@ namespace ControlCatalog.Pages
{ {
ScrollTo(_random.Next(_viewModel.Items.Count - 1)); ScrollTo(_random.Next(_viewModel.Items.Count - 1));
} }
private void scrollToSelected_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
ScrollTo(_selectedIndex);
}
} }
} }

13
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

@ -31,6 +31,19 @@ namespace ControlCatalog.ViewModels
Items.Insert(index + 1, new Item(index + 1) { Text = $"New Item {_newItemIndex++}" }); Items.Insert(index + 1, new Item(index + 1) { Text = $"New Item {_newItemIndex++}" });
} }
public void RemoveItem()
{
if (SelectedItem is not null)
{
Items.Remove(SelectedItem);
SelectedItem = null;
}
else if (Items.Count > 0)
{
Items.RemoveAt(Items.Count - 1);
}
}
public void RandomizeHeights() public void RandomizeHeights()
{ {
var random = new Random(); var random = new Random();

5
samples/SampleControls/HamburgerMenu/HamburgerMenu.cs

@ -43,14 +43,13 @@ namespace ControlSamples
_splitView = e.NameScope.Find<SplitView>("PART_NavigationPane"); _splitView = e.NameScope.Find<SplitView>("PART_NavigationPane");
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == BoundsProperty && _splitView is not null) if (change.Property == BoundsProperty && _splitView is not null)
{ {
var oldBounds = change.OldValue.GetValueOrDefault<Rect>(); var (oldBounds, newBounds) = change.GetOldAndNewValue<Rect>();
var newBounds = change.NewValue.GetValueOrDefault<Rect>();
EnsureSplitViewMode(oldBounds, newBounds); EnsureSplitViewMode(oldBounds, newBounds);
} }
} }

79
src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj

@ -1,79 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-android</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ApplicationId>com.Avalonia.AndroidTestApplication</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<DebugType>portable</DebugType>
</PropertyGroup>
<ItemGroup>
<AndroidResource Include="..\..\..\build\Assets\Icon.png">
<Link>Resources\drawable\Icon.png</Link>
</AndroidResource>
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release' and '$(TF_BUILD)' == ''">
<DebugSymbols>True</DebugSymbols>
<RunAOTCompilation>True</RunAOTCompilation>
<EnableLLVM>True</EnableLLVM>
<AndroidEnableProfiledAot>True</AndroidEnableProfiledAot>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\AboutAssets.txt" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<BundleAssemblies>True</BundleAssemblies>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<BundleAssemblies>True</BundleAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Avalonia.Android\Avalonia.Android.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj">
<Project>{7b92af71-6287-4693-9dcb-bd5b6e927e23}</Project>
<Name>Avalonia.Android</Name>
</ProjectReference>
<ProjectReference Include="..\..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj">
<Project>{3e53a01a-b331-47f3-b828-4a5717e77a24}</Project>
<Name>Avalonia.Markup.Xaml</Name>
</ProjectReference>
<ProjectReference Include="..\..\..\src\Avalonia.Base\Avalonia.Base.csproj">
<Project>{b09b78d8-9b26-48b0-9149-d64a2f120f3f}</Project>
<Name>Avalonia.Base</Name>
</ProjectReference>
<ProjectReference Include="..\..\..\src\Avalonia.Controls\Avalonia.Controls.csproj">
<Project>{d2221c82-4a25-4583-9b43-d791e3f6820c}</Project>
<Name>Avalonia.Controls</Name>
</ProjectReference>
<ProjectReference Include="..\..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj">
<Project>{7062ae20-5dcc-4442-9645-8195bdece63e}</Project>
<Name>Avalonia.Diagnostics</Name>
</ProjectReference>
<ProjectReference Include="..\..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj">
<Project>{3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f}</Project>
<Name>Avalonia.Themes.Default</Name>
</ProjectReference>
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj">
<Project>{7d2d3083-71dd-4cc9-8907-39a0d86fb322}</Project>
<Name>Avalonia.Skia</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<Import Project="..\..\..\build\Base.props" />
<Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\System.Memory.props" />
<Import Project="..\..\..\build\AndroidWorkarounds.props" />
<Import Project="..\..\..\build\LegacyProject.targets" />
</Project>

104
src/Android/Avalonia.AndroidTestApplication/MainActivity.cs

@ -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<App>
{
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;
}
}
}

4
src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:label="Avalonia.AndroidTestApplication" android:icon="@drawable/Icon" android:hardwareAccelerated="true"></application>
</manifest>

50
src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt

@ -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.

BIN
src/Android/Avalonia.AndroidTestApplication/Resources/drawable/Icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

6
src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="Hello">Hello World, Click Me!</string>
<string name="ApplicationName">Avalonia.AndroidTestApplication</string>
</resources>

7
src/Avalonia.Base/Animation/Animatable.cs

@ -87,12 +87,13 @@ namespace Avalonia.Animation
} }
} }
protected sealed override void OnPropertyChangedCore<T>(AvaloniaPropertyChangedEventArgs<T> change) protected sealed override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change)
{ {
if (change.Property == TransitionsProperty && change.IsEffectiveValueChange) if (change.Property == TransitionsProperty && change.IsEffectiveValueChange)
{ {
var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>(); var e = (AvaloniaPropertyChangedEventArgs<Transitions?>)change;
var newTransitions = change.NewValue.GetValueOrDefault<Transitions>(); var oldTransitions = e.OldValue.GetValueOrDefault();
var newTransitions = e.NewValue.GetValueOrDefault();
// When transitions are replaced, we add the new transitions before removing the old // 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 // transitions, so that when the old transition being disposed causes the value to

40
src/Avalonia.Base/Animation/Easings/Easing.cs

@ -1,8 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Linq; using Avalonia.SourceGenerator;
namespace Avalonia.Animation.Easings namespace Avalonia.Animation.Easings
{ {
@ -10,14 +11,15 @@ namespace Avalonia.Animation.Easings
/// Base class for all Easing classes. /// Base class for all Easing classes.
/// </summary> /// </summary>
[TypeConverter(typeof(EasingTypeConverter))] [TypeConverter(typeof(EasingTypeConverter))]
public abstract class Easing : IEasing public abstract partial class Easing : IEasing
{ {
/// <inheritdoc/> /// <inheritdoc/>
public abstract double Ease(double progress); public abstract double Ease(double progress);
static Dictionary<string, Type>? _easingTypes; private const string Namespace = "Avalonia.Animation.Easings";
static readonly Type s_thisType = typeof(Easing); [SubtypesFactory(typeof(Easing), Namespace)]
private static partial bool TryCreateEasingInstance(string type, [NotNullWhen(true)] out Easing? instance);
/// <summary> /// <summary>
/// Parses a Easing type string. /// Parses a Easing type string.
@ -26,34 +28,18 @@ namespace Avalonia.Animation.Easings
/// <returns>Returns the instance of the parsed type.</returns> /// <returns>Returns the instance of the parsed type.</returns>
public static Easing Parse(string e) public static Easing Parse(string e)
{ {
#if NETSTANDARD2_0
if (e.Contains(","))
#else
if (e.Contains(',')) if (e.Contains(','))
#endif
{ {
return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture)); return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture));
} }
if (_easingTypes == null) return TryCreateEasingInstance(e, out var easing)
{ ? easing
_easingTypes = new Dictionary<string, Type>(); : throw new FormatException($"Easing \"{e}\" was not found in {Namespace} namespace.");
// 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.");
}
} }
} }
} }

3
src/Avalonia.Base/ApiCompatBaseline.txt

@ -1,3 +1,4 @@
Compat issues with assembly Avalonia.Base: 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<T>(System.Action<T>, T, Avalonia.Threading.DispatcherPriority)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Threading.IDispatcher.Post<T>(System.Action<T>, T, Avalonia.Threading.DispatcherPriority)' is present in the implementation but not in the contract.
Total Issues: 1 Total Issues: 2

1
src/Avalonia.Base/Avalonia.Base.csproj

@ -16,4 +16,5 @@
<Import Project="..\..\build\ApiDiff.props" /> <Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\SourceGenerators.props" />
</Project> </Project>

77
src/Avalonia.Base/AvaloniaObject.cs

@ -5,6 +5,7 @@ using Avalonia.Data;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.PropertyStore; using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia namespace Avalonia
@ -15,13 +16,13 @@ namespace Avalonia
/// <remarks> /// <remarks>
/// This class is analogous to DependencyObject in WPF. /// This class is analogous to DependencyObject in WPF.
/// </remarks> /// </remarks>
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
{ {
private IAvaloniaObject? _inheritanceParent; private AvaloniaObject? _inheritanceParent;
private List<IDisposable>? _directBindings; private List<IDisposable>? _directBindings;
private PropertyChangedEventHandler? _inpcChanged; private PropertyChangedEventHandler? _inpcChanged;
private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChanged; private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChanged;
private List<IAvaloniaObject>? _inheritanceChildren; private List<AvaloniaObject>? _inheritanceChildren;
private ValueStore? _values; private ValueStore? _values;
private bool _batchUpdate; private bool _batchUpdate;
@ -58,7 +59,7 @@ namespace Avalonia
/// <value> /// <value>
/// The inheritance parent. /// The inheritance parent.
/// </value> /// </value>
protected IAvaloniaObject? InheritanceParent protected AvaloniaObject? InheritanceParent
{ {
get get
{ {
@ -320,14 +321,14 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param> /// <param name="priority">The priority of the value.</param>
public void SetValue( public IDisposable? SetValue(
AvaloniaProperty property, AvaloniaProperty property,
object? value, object? value,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
property.RouteSetValue(this, value, priority); return property.RouteSetValue(this, value, priority);
} }
/// <summary> /// <summary>
@ -385,6 +386,26 @@ namespace Avalonia
SetDirectValueUnchecked(property, value); SetDirectValueUnchecked(property, value);
} }
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind(
AvaloniaProperty property,
IObservable<object?> 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);
}
/// <summary> /// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable. /// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary> /// </summary>
@ -445,9 +466,8 @@ namespace Avalonia
/// <summary> /// <summary>
/// Coerces the specified <see cref="AvaloniaProperty"/>. /// Coerces the specified <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
public void CoerceValue<T>(StyledPropertyBase<T> property) public void CoerceValue(AvaloniaProperty property)
{ {
_values?.CoerceValue(property); _values?.CoerceValue(property);
} }
@ -475,19 +495,19 @@ namespace Avalonia
} }
/// <inheritdoc/> /// <inheritdoc/>
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child) internal void AddInheritanceChild(AvaloniaObject child)
{ {
_inheritanceChildren ??= new List<IAvaloniaObject>(); _inheritanceChildren ??= new List<AvaloniaObject>();
_inheritanceChildren.Add(child); _inheritanceChildren.Add(child);
} }
/// <inheritdoc/> /// <inheritdoc/>
void IAvaloniaObject.RemoveInheritanceChild(IAvaloniaObject child) internal void RemoveInheritanceChild(AvaloniaObject child)
{ {
_inheritanceChildren?.Remove(child); _inheritanceChildren?.Remove(child);
} }
void IAvaloniaObject.InheritedPropertyChanged<T>( internal void InheritedPropertyChanged<T>(
AvaloniaProperty<T> property, AvaloniaProperty<T> property,
Optional<T> oldValue, Optional<T> oldValue,
Optional<T> newValue) Optional<T> newValue)
@ -504,7 +524,7 @@ namespace Avalonia
return _propertyChanged?.GetInvocationList(); return _propertyChanged?.GetInvocationList();
} }
void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) internal void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{ {
var property = (StyledPropertyBase<T>)change.Property; var property = (StyledPropertyBase<T>)change.Property;
@ -543,7 +563,7 @@ namespace Avalonia
} }
} }
void IValueSink.Completed<T>( internal void Completed<T>(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
IPriorityValueEntry entry, IPriorityValueEntry entry,
Optional<T> oldValue) Optional<T> oldValue)
@ -554,7 +574,7 @@ namespace Avalonia
oldValue, oldValue,
default, default,
BindingPriority.Unset); BindingPriority.Unset);
((IValueSink)this).ValueChanged(change); ValueChanged(change);
} }
/// <summary> /// <summary>
@ -565,14 +585,11 @@ namespace Avalonia
/// <param name="oldParent">The old inheritance parent.</param> /// <param name="oldParent">The old inheritance parent.</param>
internal void InheritanceParentChanged<T>( internal void InheritanceParentChanged<T>(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
IAvaloniaObject? oldParent) AvaloniaObject? oldParent)
{ {
var oldValue = oldParent switch var oldValue = oldParent is not null ?
{ oldParent.GetValueOrInheritedOrDefault(property) :
AvaloniaObject o => o.GetValueOrInheritedOrDefault(property), property.GetDefaultValue(GetType());
null => property.GetDefaultValue(GetType()),
_ => oldParent.GetValue(property)
};
var newValue = GetInheritedOrDefault(property); var newValue = GetInheritedOrDefault(property);
@ -629,10 +646,12 @@ namespace Avalonia
/// enabled. /// enabled.
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The new binding value for the property.</param> /// <param name="state">The current data binding state.</param>
protected virtual void UpdateDataValidation<T>( /// <param name="error">The current data binding error, if any.</param>
AvaloniaProperty<T> property, protected virtual void UpdateDataValidation(
BindingValue<T> value) AvaloniaProperty property,
BindingValueType state,
Exception? error)
{ {
} }
@ -640,7 +659,7 @@ namespace Avalonia
/// Called when a avalonia property changes on the object. /// Called when a avalonia property changes on the object.
/// </summary> /// </summary>
/// <param name="change">The property change details.</param> /// <param name="change">The property change details.</param>
protected virtual void OnPropertyChangedCore<T>(AvaloniaPropertyChangedEventArgs<T> change) protected virtual void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change)
{ {
if (change.IsEffectiveValueChange) if (change.IsEffectiveValueChange)
{ {
@ -652,7 +671,7 @@ namespace Avalonia
/// Called when a avalonia property changes on the object. /// Called when a avalonia property changes on the object.
/// </summary> /// </summary>
/// <param name="change">The property change details.</param> /// <param name="change">The property change details.</param>
protected virtual void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
} }
@ -843,7 +862,7 @@ namespace Avalonia
if (metadata.EnableDataValidation == true) if (metadata.EnableDataValidation == true)
{ {
UpdateDataValidation(property, value); UpdateDataValidation(property, value.Type, value.Error);
} }
} }

234
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -25,7 +25,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>. /// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
@ -44,7 +44,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>. /// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <typeparam name="T">The property type.</typeparam> /// <typeparam name="T">The property type.</typeparam>
@ -64,7 +64,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>. /// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
@ -85,7 +85,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>. /// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <typeparam name="T">The property type.</typeparam> /// <typeparam name="T">The property type.</typeparam>
@ -128,7 +128,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets a subject for a <see cref="AvaloniaProperty"/>. /// Gets a subject for an <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
@ -150,7 +150,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Gets a subject for a <see cref="AvaloniaProperty"/>. /// Gets a subject for an <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>
/// <typeparam name="T">The property type.</typeparam> /// <typeparam name="T">The property type.</typeparam>
/// <param name="o">The object.</param> /// <param name="o">The object.</param>
@ -230,30 +230,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable. /// Binds an <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public static IDisposable Bind(
this IAvaloniaObject target,
AvaloniaProperty property,
IObservable<BindingValue<object?>> 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);
}
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the property.</typeparam> /// <typeparam name="T">The type of the property.</typeparam>
/// <param name="target">The object.</param> /// <param name="target">The object.</param>
@ -273,42 +250,22 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source)); source = source ?? throw new ArgumentNullException(nameof(source));
return property switch if (target is AvaloniaObject ao)
{ {
StyledPropertyBase<T> styled => target.Bind(styled, source, priority), return property switch
DirectPropertyBase<T> direct => target.Bind(direct, source), {
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), StyledPropertyBase<T> styled => ao.Bind(styled, source, priority),
}; DirectPropertyBase<T> direct => ao.Bind(direct, source),
} _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
};
}
/// <summary> throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
public static IDisposable Bind(
this IAvaloniaObject target,
AvaloniaProperty property,
IObservable<object?> 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 target.Bind(
property,
source.ToBindingValue(),
priority);
} }
/// <summary> /// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable. /// Binds an <see cref="AvaloniaProperty"/> to an observable.
/// </summary> /// </summary>
/// <param name="target">The object.</param> /// <param name="target">The object.</param>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
@ -334,7 +291,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Binds a property on an <see cref="IAvaloniaObject"/> to an <see cref="IBinding"/>. /// Binds a property on an <see cref="AvaloniaObject"/> to an <see cref="IBinding"/>.
/// </summary> /// </summary>
/// <param name="target">The object.</param> /// <param name="target">The object.</param>
/// <param name="property">The property to bind.</param> /// <param name="property">The property to bind.</param>
@ -374,56 +331,6 @@ namespace Avalonia
} }
} }
/// <summary>
/// Clears a <see cref="AvaloniaProperty"/>'s local value.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
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);
}
/// <summary>
/// Clears a <see cref="AvaloniaProperty"/>'s local value.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
public static void ClearValue<T>(this IAvaloniaObject target, AvaloniaProperty<T> property)
{
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
switch (property)
{
case StyledPropertyBase<T> styled:
target.ClearValue(styled);
break;
case DirectPropertyBase<T> direct:
target.ClearValue(direct);
break;
default:
throw new NotSupportedException("Unsupported AvaloniaProperty type.");
}
}
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
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);
}
/// <summary> /// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value. /// Gets a <see cref="AvaloniaProperty"/> value.
/// </summary> /// </summary>
@ -436,12 +343,18 @@ namespace Avalonia
target = target ?? throw new ArgumentNullException(nameof(target)); target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
return property switch if (target is AvaloniaObject ao)
{ {
StyledPropertyBase<T> styled => target.GetValue(styled), return property switch
DirectPropertyBase<T> direct => target.GetValue(direct), {
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") StyledPropertyBase<T> styled => ao.GetValue(styled),
}; DirectPropertyBase<T> direct => ao.GetValue(direct),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
};
}
throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
} }
/// <summary> /// <summary>
@ -456,7 +369,7 @@ namespace Avalonia
/// <see cref="AvaloniaProperty.UnsetValue"/>. Note that this method does not return /// <see cref="AvaloniaProperty.UnsetValue"/>. Note that this method does not return
/// property values that come from inherited or default values. /// property values that come from inherited or default values.
/// ///
/// For direct properties returns <see cref="GetValue(IAvaloniaObject, AvaloniaProperty)"/>. /// For direct properties returns the current value of the property.
/// </remarks> /// </remarks>
public static object? GetBaseValue( public static object? GetBaseValue(
this IAvaloniaObject target, this IAvaloniaObject target,
@ -466,7 +379,9 @@ namespace Avalonia
target = target ?? throw new ArgumentNullException(nameof(target)); target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property)); 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.");
} }
/// <summary> /// <summary>
@ -481,8 +396,7 @@ namespace Avalonia
/// <see cref="Optional{T}.Empty"/>. Note that this method does not return property values /// <see cref="Optional{T}.Empty"/>. Note that this method does not return property values
/// that come from inherited or default values. /// that come from inherited or default values.
/// ///
/// For direct properties returns /// For direct properties returns the current value of the property.
/// <see cref="IAvaloniaObject.GetValue{T}(DirectPropertyBase{T})"/>.
/// </remarks> /// </remarks>
public static Optional<T> GetBaseValue<T>( public static Optional<T> GetBaseValue<T>(
this IAvaloniaObject target, this IAvaloniaObject target,
@ -492,69 +406,18 @@ namespace Avalonia
target = target ?? throw new ArgumentNullException(nameof(target)); target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
target = target ?? throw new ArgumentNullException(nameof(target)); if (target is AvaloniaObject ao)
property = property ?? throw new ArgumentNullException(nameof(property));
return property switch
{ {
StyledPropertyBase<T> styled => target.GetBaseValue(styled, maxPriority), return property switch
DirectPropertyBase<T> direct => target.GetValue(direct), {
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") StyledPropertyBase<T> styled => ao.GetBaseValue(styled, maxPriority),
}; DirectPropertyBase<T> direct => ao.GetValue(direct),
} _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
};
/// <summary>
/// Sets a <see cref="AvaloniaProperty"/> value.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param>
/// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
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);
}
/// <summary>
/// Sets a <see cref="AvaloniaProperty"/> value.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="target">The object.</param>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param>
/// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
public static IDisposable? SetValue<T>(
this IAvaloniaObject target,
AvaloniaProperty<T> property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
switch (property)
{
case StyledPropertyBase<T> styled:
return target.SetValue(styled, value, priority);
case DirectPropertyBase<T> direct:
target.SetValue(direct, value);
return null;
default:
throw new NotSupportedException("Unsupported AvaloniaProperty type.");
} }
throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
} }
/// <summary> /// <summary>
@ -622,17 +485,6 @@ namespace Avalonia
return observable.Subscribe(e => SubscribeAdapter(e, handler)); return observable.Subscribe(e => SubscribeAdapter(e, handler));
} }
/// <summary>
/// Gets a description of a property that van be used in observables.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property</param>
/// <returns>The description.</returns>
private static string GetDescription(IAvaloniaObject o, AvaloniaProperty property)
{
return $"{o.GetType().Name}.{property.Name}";
}
/// <summary> /// <summary>
/// Observer method for <see cref="AddClassHandler{TTarget}(IObservable{AvaloniaPropertyChangedEventArgs}, /// Observer method for <see cref="AddClassHandler{TTarget}(IObservable{AvaloniaPropertyChangedEventArgs},
/// Func{TTarget, Action{AvaloniaPropertyChangedEventArgs}})"/>. /// Func{TTarget, Action{AvaloniaPropertyChangedEventArgs}})"/>.

23
src/Avalonia.Base/AvaloniaProperty.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Styling;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
@ -454,33 +455,24 @@ namespace Avalonia
return Name; return Name;
} }
/// <summary>
/// Uses the visitor pattern to resolve an untyped property to a typed property.
/// </summary>
/// <typeparam name="TData">The type of user data passed.</typeparam>
/// <param name="visitor">The visitor which will accept the typed property.</param>
/// <param name="data">The user data to pass.</param>
public abstract void Accept<TData>(IAvaloniaPropertyVisitor<TData> visitor, ref TData data)
where TData : struct;
/// <summary> /// <summary>
/// Routes an untyped ClearValue call to a typed call. /// Routes an untyped ClearValue call to a typed call.
/// </summary> /// </summary>
/// <param name="o">The object instance.</param> /// <param name="o">The object instance.</param>
internal abstract void RouteClearValue(IAvaloniaObject o); internal abstract void RouteClearValue(AvaloniaObject o);
/// <summary> /// <summary>
/// Routes an untyped GetValue call to a typed call. /// Routes an untyped GetValue call to a typed call.
/// </summary> /// </summary>
/// <param name="o">The object instance.</param> /// <param name="o">The object instance.</param>
internal abstract object? RouteGetValue(IAvaloniaObject o); internal abstract object? RouteGetValue(AvaloniaObject o);
/// <summary> /// <summary>
/// Routes an untyped GetBaseValue call to a typed call. /// Routes an untyped GetBaseValue call to a typed call.
/// </summary> /// </summary>
/// <param name="o">The object instance.</param> /// <param name="o">The object instance.</param>
/// <param name="maxPriority">The maximum priority for the value.</param> /// <param name="maxPriority">The maximum priority for the value.</param>
internal abstract object? RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority); internal abstract object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority);
/// <summary> /// <summary>
/// Routes an untyped SetValue call to a typed call. /// Routes an untyped SetValue call to a typed call.
@ -492,7 +484,7 @@ namespace Avalonia
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null. /// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns> /// </returns>
internal abstract IDisposable? RouteSetValue( internal abstract IDisposable? RouteSetValue(
IAvaloniaObject o, AvaloniaObject o,
object? value, object? value,
BindingPriority priority); BindingPriority priority);
@ -503,11 +495,12 @@ namespace Avalonia
/// <param name="source">The binding source.</param> /// <param name="source">The binding source.</param>
/// <param name="priority">The priority.</param> /// <param name="priority">The priority.</param>
internal abstract IDisposable RouteBind( internal abstract IDisposable RouteBind(
IAvaloniaObject o, AvaloniaObject o,
IObservable<BindingValue<object?>> source, IObservable<BindingValue<object?>> source,
BindingPriority priority); 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);
/// <summary> /// <summary>
/// Overrides the metadata for the property on the specified type. /// Overrides the metadata for the property on the specified type.

43
src/Avalonia.Base/AvaloniaPropertyChangedExtensions.cs

@ -0,0 +1,43 @@
namespace Avalonia
{
/// <summary>
/// Provides extensions for <see cref="AvaloniaPropertyChangedEventArgs"/>.
/// </summary>
public static class AvaloniaPropertyChangedExtensions
{
/// <summary>
/// Gets a typed value from <see cref="AvaloniaPropertyChangedEventArgs.OldValue"/>.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="e">The event args.</param>
/// <returns>The value.</returns>
public static T GetOldValue<T>(this AvaloniaPropertyChangedEventArgs e)
{
return ((AvaloniaPropertyChangedEventArgs<T>)e).OldValue.GetValueOrDefault()!;
}
/// <summary>
/// Gets a typed value from <see cref="AvaloniaPropertyChangedEventArgs.NewValue"/>.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="e">The event args.</param>
/// <returns>The value.</returns>
public static T GetNewValue<T>(this AvaloniaPropertyChangedEventArgs e)
{
return ((AvaloniaPropertyChangedEventArgs<T>)e).NewValue.GetValueOrDefault()!;
}
/// <summary>
/// Gets a typed value from <see cref="AvaloniaPropertyChangedEventArgs.OldValue"/> and
/// <see cref="AvaloniaPropertyChangedEventArgs.NewValue"/>.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="e">The event args.</param>
/// <returns>The value.</returns>
public static (T oldValue, T newValue) GetOldAndNewValue<T>(this AvaloniaPropertyChangedEventArgs e)
{
var ev = (AvaloniaPropertyChangedEventArgs<T>)e;
return (ev.OldValue.GetValueOrDefault()!, ev.NewValue.GetValueOrDefault()!);
}
}
}

44
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Styling;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
@ -121,31 +122,25 @@ namespace Avalonia
} }
/// <inheritdoc/> /// <inheritdoc/>
public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> visitor, ref TData data) internal override void RouteClearValue(AvaloniaObject o)
{
visitor.Visit(this, ref data);
}
/// <inheritdoc/>
internal override void RouteClearValue(IAvaloniaObject o)
{ {
o.ClearValue<TValue>(this); o.ClearValue<TValue>(this);
} }
/// <inheritdoc/> /// <inheritdoc/>
internal override object? RouteGetValue(IAvaloniaObject o) internal override object? RouteGetValue(AvaloniaObject o)
{ {
return o.GetValue<TValue>(this); return o.GetValue<TValue>(this);
} }
internal override object? RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority)
{ {
return o.GetValue<TValue>(this); return o.GetValue<TValue>(this);
} }
/// <inheritdoc/> /// <inheritdoc/>
internal override IDisposable? RouteSetValue( internal override IDisposable? RouteSetValue(
IAvaloniaObject o, AvaloniaObject o,
object? value, object? value,
BindingPriority priority) BindingPriority priority)
{ {
@ -169,7 +164,7 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
internal override IDisposable RouteBind( internal override IDisposable RouteBind(
IAvaloniaObject o, AvaloniaObject o,
IObservable<BindingValue<object?>> source, IObservable<BindingValue<object?>> source,
BindingPriority priority) BindingPriority priority)
{ {
@ -177,9 +172,34 @@ namespace Avalonia
return o.Bind<TValue>(this, adapter); return o.Bind<TValue>(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."); 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<TValue>(
target,
this,
binding);
}
else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType))
{
return new PropertySetterLazyInstance<TValue>(
target,
this,
() => (TValue)template.Build());
}
else
{
return new PropertySetterInstance<TValue>(
target,
this,
(TValue)value!);
}
}
} }
} }

2
src/Avalonia.Base/GeometryGroup.cs

@ -68,7 +68,7 @@ namespace Avalonia.Media
return null; return null;
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);

104
src/Avalonia.Base/IAvaloniaObject.cs

@ -17,42 +17,14 @@ namespace Avalonia
/// Clears an <see cref="AvaloniaProperty"/>'s local value. /// Clears an <see cref="AvaloniaProperty"/>'s local value.
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
void ClearValue<T>(StyledPropertyBase<T> property); void ClearValue(AvaloniaProperty property);
/// <summary>
/// Clears an <see cref="AvaloniaProperty"/>'s local value.
/// </summary>
/// <param name="property">The property.</param>
void ClearValue<T>(DirectPropertyBase<T> property);
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
T GetValue<T>(StyledPropertyBase<T> property);
/// <summary> /// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value. /// Gets a <see cref="AvaloniaProperty"/> value.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <returns>The value.</returns> /// <returns>The value.</returns>
T GetValue<T>(DirectPropertyBase<T> property); object? GetValue(AvaloniaProperty property);
/// <summary>
/// Gets an <see cref="AvaloniaProperty"/> base value.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="maxPriority">The maximum priority for the value.</param>
/// <remarks>
/// Gets the value of the property, if set on this object with a priority equal or lower to
/// <paramref name="maxPriority"/>, otherwise <see cref="Optional{T}.Empty"/>. Note that
/// this method does not return property values that come from inherited or default values.
/// </remarks>
Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property, BindingPriority maxPriority);
/// <summary> /// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is animating. /// Checks whether a <see cref="AvaloniaProperty"/> is animating.
@ -71,93 +43,35 @@ namespace Avalonia
/// <summary> /// <summary>
/// Sets a <see cref="AvaloniaProperty"/> value. /// Sets a <see cref="AvaloniaProperty"/> value.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param> /// <param name="priority">The priority of the value.</param>
/// <returns> /// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null. /// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns> /// </returns>
IDisposable? SetValue<T>( IDisposable? SetValue(
StyledPropertyBase<T> property, AvaloniaProperty property,
T value, object? value,
BindingPriority priority = BindingPriority.LocalValue); BindingPriority priority = BindingPriority.LocalValue);
/// <summary>
/// Sets a <see cref="AvaloniaProperty"/> value.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
void SetValue<T>(DirectPropertyBase<T> property, T value);
/// <summary> /// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable. /// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="source">The observable.</param> /// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param> /// <param name="priority">The priority of the binding.</param>
/// <returns> /// <returns>
/// A disposable which can be used to terminate the binding. /// A disposable which can be used to terminate the binding.
/// </returns> /// </returns>
IDisposable Bind<T>( IDisposable Bind(
StyledPropertyBase<T> property, AvaloniaProperty property,
IObservable<BindingValue<T>> source, IObservable<object?> source,
BindingPriority priority = BindingPriority.LocalValue); BindingPriority priority = BindingPriority.LocalValue);
/// <summary>
/// Binds a <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
IDisposable Bind<T>(
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source);
/// <summary> /// <summary>
/// Coerces the specified <see cref="AvaloniaProperty"/>. /// Coerces the specified <see cref="AvaloniaProperty"/>.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
void CoerceValue<T>(StyledPropertyBase<T> property); void CoerceValue(AvaloniaProperty property);
/// <summary>
/// Registers an object as an inheritance child.
/// </summary>
/// <param name="child">The inheritance child.</param>
/// <remarks>
/// Inheritance children will receive a call to
/// <see cref="InheritedPropertyChanged{T}(AvaloniaProperty{T}, Optional{T}, Optional{T})"/>
/// when an inheritable property value changes on the parent.
/// </remarks>
void AddInheritanceChild(IAvaloniaObject child);
/// <summary>
/// Unregisters an object as an inheritance child.
/// </summary>
/// <param name="child">The inheritance child.</param>
/// <remarks>
/// Removes an inheritance child that was added by a call to
/// <see cref="AddInheritanceChild(IAvaloniaObject)"/>.
/// </remarks>
void RemoveInheritanceChild(IAvaloniaObject child);
/// <summary>
/// Called when an inheritable property changes on an object registered as an inheritance
/// parent.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <param name="property">The property that has changed.</param>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
void InheritedPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
Optional<T> newValue);
} }
} }

3
src/Avalonia.Base/Input/IInputRoot.cs

@ -1,5 +1,3 @@
using JetBrains.Annotations;
namespace Avalonia.Input namespace Avalonia.Input
{ {
/// <summary> /// <summary>
@ -30,7 +28,6 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Gets associated mouse device /// Gets associated mouse device
/// </summary> /// </summary>
[CanBeNull]
IMouseDevice? MouseDevice { get; } IMouseDevice? MouseDevice { get; }
} }
} }

6
src/Avalonia.Base/Input/IKeyboardDevice.cs

@ -50,12 +50,6 @@ namespace Avalonia.Input
KeyboardMask = Alt | Control | Shift | Meta KeyboardMask = Alt | Control | Shift | Meta
} }
internal static class KeyModifiersUtils
{
public static KeyModifiers ConvertToKey(RawInputModifiers modifiers) =>
(KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
}
public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged
{ {
IInputElement? FocusedElement { get; } IInputElement? FocusedElement { get; }

2
src/Avalonia.Base/Input/IMouseDevice.cs

@ -13,8 +13,10 @@ namespace Avalonia.Input
[Obsolete("Use PointerEventArgs.GetPosition")] [Obsolete("Use PointerEventArgs.GetPosition")]
PixelPoint Position { get; } PixelPoint Position { get; }
[Obsolete]
void TopLevelClosed(IInputRoot root); void TopLevelClosed(IInputRoot root);
[Obsolete]
void SceneInvalidated(IInputRoot root, Rect rect); void SceneInvalidated(IInputRoot root, Rect rect);
} }
} }

16
src/Avalonia.Base/Input/IPointerDevice.cs

@ -1,17 +1,31 @@
using System; using System;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Input.Raw;
namespace Avalonia.Input namespace Avalonia.Input
{ {
public interface IPointerDevice : IInputDevice public interface IPointerDevice : IInputDevice
{ {
/// <inheritdoc cref="IPointer.Captured" />
[Obsolete("Use IPointer")] [Obsolete("Use IPointer")]
IInputElement? Captured { get; } IInputElement? Captured { get; }
/// <inheritdoc cref="IPointer.Capture(IInputElement?)" />
[Obsolete("Use IPointer")] [Obsolete("Use IPointer")]
void Capture(IInputElement? control); void Capture(IInputElement? control);
/// <inheritdoc cref="PointerEventArgs.GetPosition(IVisual?)" />
[Obsolete("Use PointerEventArgs.GetPosition")] [Obsolete("Use PointerEventArgs.GetPosition")]
Point GetPosition(IVisual relativeTo); Point GetPosition(IVisual relativeTo);
/// <summary>
/// Gets a pointer for specific event args.
/// </summary>
/// <remarks>
/// If pointer doesn't exist or wasn't yet created this method will return null.
/// </remarks>
/// <param name="ev">Raw pointer event args associated with the pointer.</param>
/// <returns>The pointer.</returns>
IPointer? TryGetPointer(RawPointerEventArgs ev);
} }
} }

8
src/Avalonia.Base/Input/InputElement.cs

@ -601,21 +601,21 @@ namespace Avalonia.Input
{ {
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == IsFocusedProperty) if (change.Property == IsFocusedProperty)
{ {
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>(), null); UpdatePseudoClasses(change.GetNewValue<bool>(), null);
} }
else if (change.Property == IsPointerOverProperty) else if (change.Property == IsPointerOverProperty)
{ {
UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault<bool>()); UpdatePseudoClasses(null, change.GetNewValue<bool>());
} }
else if (change.Property == IsKeyboardFocusWithinProperty) else if (change.Property == IsKeyboardFocusWithinProperty)
{ {
PseudoClasses.Set(":focus-within", change.NewValue.GetValueOrDefault<bool>()); PseudoClasses.Set(":focus-within", change.GetNewValue<bool>());
} }
} }

5
src/Avalonia.Base/Input/KeyGesture.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Input namespace Avalonia.Input
{ {
@ -155,7 +156,7 @@ namespace Avalonia.Input
if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv)) if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv))
return rv; return rv;
return (Key)Enum.Parse(typeof(Key), key, true); return EnumHelper.Parse<Key>(key, true);
} }
private static KeyModifiers ParseModifier(ReadOnlySpan<char> modifier) private static KeyModifiers ParseModifier(ReadOnlySpan<char> modifier)
@ -172,7 +173,7 @@ namespace Avalonia.Input
return KeyModifiers.Meta; return KeyModifiers.Meta;
} }
return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true); return EnumHelper.Parse<KeyModifiers>(modifier.ToString(), true);
} }
private Key ResolveNumPadOperationKey(Key key) private Key ResolveNumPadOperationKey(Key key)

2
src/Avalonia.Base/Input/KeyboardDevice.cs

@ -188,7 +188,7 @@ namespace Avalonia.Input
RoutedEvent = routedEvent, RoutedEvent = routedEvent,
Device = this, Device = this,
Key = keyInput.Key, Key = keyInput.Key,
KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), KeyModifiers = keyInput.Modifiers.ToKeyModifiers(),
Source = element, Source = element,
}; };

2
src/Avalonia.Base/Input/KeyboardNavigation.cs

@ -58,7 +58,7 @@ namespace Avalonia.Input
/// <returns>The <see cref="KeyboardNavigationMode"/> for the container.</returns> /// <returns>The <see cref="KeyboardNavigationMode"/> for the container.</returns>
public static int GetTabIndex(IInputElement element) public static int GetTabIndex(IInputElement element)
{ {
return ((IAvaloniaObject)element).GetValue(TabIndexProperty); return ((AvaloniaObject)element).GetValue(TabIndexProperty);
} }
/// <summary> /// <summary>

328
src/Avalonia.Base/Input/MouseDevice.cs

@ -21,27 +21,17 @@ namespace Avalonia.Input
private readonly Pointer _pointer; private readonly Pointer _pointer;
private bool _disposed; private bool _disposed;
private PixelPoint? _position; private PixelPoint? _position;
private MouseButton _lastMouseDownButton;
public MouseDevice(Pointer? pointer = null) public MouseDevice(Pointer? pointer = null)
{ {
_pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
} }
/// <summary>
/// Gets the control that is currently capturing by the mouse, if any.
/// </summary>
/// <remarks>
/// 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
/// <see cref="Capture"/> method.
/// </remarks>
[Obsolete("Use IPointer instead")] [Obsolete("Use IPointer instead")]
public IInputElement? Captured => _pointer.Captured; public IInputElement? Captured => _pointer.Captured;
/// <summary>
/// Gets the mouse position, in screen coordinates.
/// </summary>
[Obsolete("Use events instead")] [Obsolete("Use events instead")]
public PixelPoint Position public PixelPoint Position
{ {
@ -49,15 +39,7 @@ namespace Avalonia.Input
protected set => _position = value; protected set => _position = value;
} }
/// <summary> [Obsolete("Use IPointer instead")]
/// Captures mouse input to the specified control.
/// </summary>
/// <param name="control">The control.</param>
/// <remarks>
/// 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 <see cref="Captured"/> property.
/// </remarks>
public void Capture(IInputElement? control) public void Capture(IInputElement? control)
{ {
_pointer.Capture(control); _pointer.Capture(control);
@ -90,39 +72,6 @@ namespace Avalonia.Input
ProcessRawEvent(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) int ButtonCount(PointerPointProperties props)
{ {
var rv = 0; var rv = 0;
@ -138,7 +87,7 @@ namespace Avalonia.Input
rv++; rv++;
return rv; return rv;
} }
private void ProcessRawEvent(RawPointerEventArgs e) private void ProcessRawEvent(RawPointerEventArgs e)
{ {
e = e ?? throw new ArgumentNullException(nameof(e)); e = e ?? throw new ArgumentNullException(nameof(e));
@ -147,15 +96,14 @@ namespace Avalonia.Input
if(mouse._disposed) if(mouse._disposed)
return; return;
if (e.Type == RawPointerEventType.NonClientLeftButtonDown) return;
_position = e.Root.PointToScreen(e.Position); _position = e.Root.PointToScreen(e.Position);
var props = CreateProperties(e); var props = CreateProperties(e);
var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); var keyModifiers = e.InputModifiers.ToKeyModifiers();
switch (e.Type) switch (e.Type)
{ {
case RawPointerEventType.LeaveWindow: case RawPointerEventType.LeaveWindow:
LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); case RawPointerEventType.NonClientLeftButtonDown:
LeaveWindow();
break; break;
case RawPointerEventType.LeftButtonDown: case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown: case RawPointerEventType.RightButtonDown:
@ -163,10 +111,9 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Down: case RawPointerEventType.XButton1Down:
case RawPointerEventType.XButton2Down: case RawPointerEventType.XButton2Down:
if (ButtonCount(props) > 1) if (ButtonCount(props) > 1)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
else else
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
props, keyModifiers);
break; break;
case RawPointerEventType.LeftButtonUp: case RawPointerEventType.LeftButtonUp:
case RawPointerEventType.RightButtonUp: case RawPointerEventType.RightButtonUp:
@ -174,82 +121,50 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Up: case RawPointerEventType.XButton1Up:
case RawPointerEventType.XButton2Up: case RawPointerEventType.XButton2Up:
if (ButtonCount(props) != 0) if (ButtonCount(props) != 0)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
else else
e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
break; break;
case RawPointerEventType.Move: case RawPointerEventType.Move:
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
break; break;
case RawPointerEventType.Wheel: case RawPointerEventType.Wheel:
e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break; break;
case RawPointerEventType.Magnify: case RawPointerEventType.Magnify:
e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break; break;
case RawPointerEventType.Rotate: case RawPointerEventType.Rotate:
e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break; break;
case RawPointerEventType.Swipe: case RawPointerEventType.Swipe:
e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break; break;
} }
} }
private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, private void LeaveWindow()
KeyModifiers inputModifiers)
{ {
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
_position = null; _position = null;
ClearPointerOver(this, timestamp, root, properties, inputModifiers);
} }
PointerPointProperties CreateProperties(RawPointerEventArgs args) PointerPointProperties CreateProperties(RawPointerEventArgs args)
{ {
return new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind());
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, private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p,
PointerPointProperties properties, PointerPointProperties properties,
KeyModifiers inputModifiers) KeyModifiers inputModifiers, IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var source = _pointer.Captured ?? root.InputHitTest(p);
if (hit != null) if (source != null)
{ {
_pointer.Capture(hit); _pointer.Capture(source);
var source = GetSource(hit);
if (source != null) if (source != null)
{ {
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>(); var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
@ -275,23 +190,14 @@ namespace Avalonia.Input
return false; return false;
} }
private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
KeyModifiers inputModifiers, Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints) PointerPointProperties properties, KeyModifiers inputModifiers, Lazy<IReadOnlyList<RawPointerPoint>?>? intermediatePoints,
IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
IInputElement? source; var source = _pointer.Captured ?? hitTest;
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) if (source is object)
{ {
@ -306,13 +212,12 @@ namespace Avalonia.Input
} }
private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props,
KeyModifiers inputModifiers) KeyModifiers inputModifiers, IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var source = _pointer.Captured ?? hitTest;
var source = GetSource(hit);
if (source is not null) if (source is not null)
{ {
@ -329,13 +234,12 @@ namespace Avalonia.Input
private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, PointerPointProperties props,
Vector delta, KeyModifiers inputModifiers) Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var source = _pointer.Captured ?? hitTest;
var source = GetSource(hit);
// KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform. // 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 Shift-Key is pressed and X is close to 0 we swap the Vector.
@ -356,16 +260,15 @@ namespace Avalonia.Input
} }
private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var source = _pointer.Captured ?? hitTest;
if (hit != null) if (source != null)
{ {
var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source, var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta); _pointer, root, p, timestamp, props, inputModifiers, delta);
@ -377,16 +280,15 @@ namespace Avalonia.Input
} }
private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var source = _pointer.Captured ?? hitTest;
if (hit != null) if (source != null)
{ {
var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source, var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta); _pointer, root, p, timestamp, props, inputModifiers, delta);
@ -398,16 +300,15 @@ namespace Avalonia.Input
} }
private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root)); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var source = _pointer.Captured ?? hitTest;
if (hit != null) if (source != null)
{ {
var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source, var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta); _pointer, root, p, timestamp, props, inputModifiers, delta);
@ -418,154 +319,27 @@ namespace Avalonia.Input
return false; return false;
} }
private IInteractive? GetSource(IVisual? hit) public void Dispose()
{
if (hit is null)
return null;
return _pointer.Captured ??
(hit as IInteractive) ??
hit.GetSelfAndVisualAncestors().OfType<IInteractive>().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) _disposed = true;
{ _pointer?.Dispose();
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, [Obsolete]
PointerPointProperties properties, public void TopLevelClosed(IInputRoot root)
KeyModifiers inputModifiers)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); // no-op
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, [Obsolete]
PointerPointProperties properties, public void SceneInvalidated(IInputRoot root, Rect rect)
KeyModifiers inputModifiers)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); // no-op
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() public IPointer? TryGetPointer(RawPointerEventArgs ev)
{ {
_disposed = true; return _pointer;
_pointer?.Dispose();
} }
} }
} }

5
src/Avalonia.Base/Input/Navigation/TabNavigation.cs

@ -610,7 +610,7 @@ namespace Avalonia.Input.Navigation
private static IInputElement? GetActiveElement(IInputElement e) private static IInputElement? GetActiveElement(IInputElement e)
{ {
return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabOnceActiveElementProperty); return ((AvaloniaObject)e).GetValue(KeyboardNavigation.TabOnceActiveElementProperty);
} }
private static IInputElement GetGroupParent(IInputElement e) => GetGroupParent(e, false); private static IInputElement GetGroupParent(IInputElement e) => GetGroupParent(e, false);
@ -655,8 +655,9 @@ namespace Avalonia.Input.Navigation
private static KeyboardNavigationMode GetKeyNavigationMode(IInputElement e) private static KeyboardNavigationMode GetKeyNavigationMode(IInputElement e)
{ {
return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabNavigationProperty); return ((AvaloniaObject)e).GetValue(KeyboardNavigation.TabNavigationProperty);
} }
private static bool IsFocusScope(IInputElement e) => FocusManager.GetIsFocusScope(e) || GetParent(e) == null; 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 IsGroup(IInputElement e) => GetKeyNavigationMode(e) != KeyboardNavigationMode.Continue;

2
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -63,6 +63,8 @@ namespace Avalonia.Input
} }
public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo);
public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
} }
public IPointer Pointer { get; } public IPointer Pointer { get; }

209
src/Avalonia.Base/Input/PointerOverPreProcessor.cs

@ -0,0 +1,209 @@
using System;
using Avalonia.Input.Raw;
namespace Avalonia.Input
{
internal class PointerOverPreProcessor : IObserver<RawInputEventArgs>
{
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;
}
}
}
}

2
src/Avalonia.Base/Input/Raw/RawDragEvent.cs

@ -20,7 +20,7 @@ namespace Avalonia.Input.Raw
Location = location; Location = location;
Data = data; Data = data;
Effects = effects; Effects = effects;
KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); KeyModifiers = modifiers.ToKeyModifiers();
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
Modifiers = (InputModifiers)modifiers; Modifiers = (InputModifiers)modifiers;
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete

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

2
src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs

@ -120,6 +120,8 @@ namespace Avalonia.Input.Raw
/// only valid for Move and TouchUpdate /// only valid for Move and TouchUpdate
/// </summary> /// </summary>
public Lazy<IReadOnlyList<RawPointerPoint>?>? IntermediatePoints { get; set; } public Lazy<IReadOnlyList<RawPointerPoint>?>? IntermediatePoints { get; set; }
internal IInputElement? InputHitTestResult { get; set; }
} }
public struct RawPointerPoint public struct RawPointerPoint

47
src/Avalonia.Base/Input/TouchDevice.cs

@ -3,24 +3,26 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
{ {
/// <summary> /// <summary>
/// Handles raw touch events /// Handles raw touch events
/// </summary>
/// <remarks> /// <remarks>
/// This class is supposed to be used on per-toplevel basis, don't use a shared one /// This class is supposed to be used on per-toplevel basis, don't use a shared one
/// </remarks> /// </remarks>
/// </summary> public class TouchDevice : IPointerDevice, IDisposable
public class TouchDevice : IInputDevice, IDisposable
{ {
private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>(); private readonly Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
private bool _disposed; private bool _disposed;
private int _clickCount; private int _clickCount;
private Rect _lastClickRect; private Rect _lastClickRect;
private ulong _lastClickTime; private ulong _lastClickTime;
KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => private Pointer? _lastPointer;
(KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
IInputElement? IPointerDevice.Captured => _lastPointer?.Captured;
RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown) RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown)
{ {
@ -30,6 +32,10 @@ namespace Avalonia.Input
return rv; return rv;
} }
void IPointerDevice.Capture(IInputElement? control) => _lastPointer?.Capture(control);
Point IPointerDevice.GetPosition(IVisual relativeTo) => default;
public void ProcessRawEvent(RawInputEventArgs ev) public void ProcessRawEvent(RawInputEventArgs ev)
{ {
if (ev.Handled || _disposed) if (ev.Handled || _disposed)
@ -39,15 +45,18 @@ namespace Avalonia.Input
{ {
if (args.Type == RawPointerEventType.TouchEnd) if (args.Type == RawPointerEventType.TouchEnd)
return; return;
var hit = args.Root.InputHitTest(args.Position); var hit = args.InputHitTestResult;
_pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(),
PointerType.Touch, _pointers.Count == 0); PointerType.Touch, _pointers.Count == 0);
pointer.Capture(hit); pointer.Capture(hit);
} }
_lastPointer = pointer;
var target = pointer.Captured ?? args.Root; var target = pointer.Captured ?? args.Root;
var updateKind = args.Type.ToUpdateKind();
var keyModifier = args.InputModifiers.ToKeyModifiers();
if (args.Type == RawPointerEventType.TouchBegin) if (args.Type == RawPointerEventType.TouchBegin)
{ {
if (_pointers.Count > 1) if (_pointers.Count > 1)
@ -73,9 +82,8 @@ namespace Avalonia.Input
target.RaiseEvent(new PointerPressedEventArgs(target, pointer, target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp, args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, true), new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind),
PointerUpdateKind.LeftButtonPressed), keyModifier, _clickCount));
GetKeyModifiers(args.InputModifiers), _clickCount));
} }
if (args.Type == RawPointerEventType.TouchEnd) if (args.Type == RawPointerEventType.TouchEnd)
@ -85,10 +93,10 @@ namespace Avalonia.Input
{ {
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp, args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, false), new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind),
PointerUpdateKind.LeftButtonReleased), keyModifier, MouseButton.Left));
GetKeyModifiers(args.InputModifiers), MouseButton.Left));
} }
_lastPointer = null;
} }
if (args.Type == RawPointerEventType.TouchCancel) if (args.Type == RawPointerEventType.TouchCancel)
@ -96,18 +104,16 @@ namespace Avalonia.Input
_pointers.Remove(args.TouchPointId); _pointers.Remove(args.TouchPointId);
using (pointer) using (pointer)
pointer.Capture(null); pointer.Capture(null);
_lastPointer = null;
} }
if (args.Type == RawPointerEventType.TouchUpdate) if (args.Type == RawPointerEventType.TouchUpdate)
{ {
var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, ev.Timestamp, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other), new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind),
GetKeyModifiers(args.InputModifiers), args.IntermediatePoints)); keyModifier, args.IntermediatePoints));
} }
} }
public void Dispose() public void Dispose()
@ -121,5 +127,12 @@ namespace Avalonia.Input
p.Dispose(); p.Dispose();
} }
public IPointer? TryGetPointer(RawPointerEventArgs ev)
{
return ev is RawTouchEventArgs args
&& _pointers.TryGetValue(args.TouchPointId, out var pointer)
? pointer
: null;
}
} }
} }

24
src/Avalonia.Base/Interactivity/IInteractive.cs

@ -28,21 +28,6 @@ namespace Avalonia.Interactivity
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false); bool handledEventsToo = false);
/// <summary>
/// Adds a handler for the specified routed event.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event's args.</typeparam>
/// <param name="routedEvent">The routed event.</param>
/// <param name="handler">The handler.</param>
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
/// <returns>A disposable that terminates the event subscription.</returns>
void AddHandler<TEventArgs>(
RoutedEvent<TEventArgs> routedEvent,
EventHandler<TEventArgs> handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) where TEventArgs : RoutedEventArgs;
/// <summary> /// <summary>
/// Removes a handler for the specified routed event. /// Removes a handler for the specified routed event.
/// </summary> /// </summary>
@ -50,15 +35,6 @@ namespace Avalonia.Interactivity
/// <param name="handler">The handler.</param> /// <param name="handler">The handler.</param>
void RemoveHandler(RoutedEvent routedEvent, Delegate handler); void RemoveHandler(RoutedEvent routedEvent, Delegate handler);
/// <summary>
/// Removes a handler for the specified routed event.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event's args.</typeparam>
/// <param name="routedEvent">The routed event.</param>
/// <param name="handler">The handler.</param>
void RemoveHandler<TEventArgs>(RoutedEvent<TEventArgs> routedEvent, EventHandler<TEventArgs> handler)
where TEventArgs : RoutedEventArgs;
/// <summary> /// <summary>
/// Adds the object's handlers for a routed event to an event route. /// Adds the object's handlers for a routed event to an event route.
/// </summary> /// </summary>

4
src/Avalonia.Base/Layout/StackLayout.cs

@ -320,11 +320,11 @@ namespace Avalonia.Layout
InvalidateLayout(); InvalidateLayout();
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
if (change.Property == OrientationProperty) if (change.Property == OrientationProperty)
{ {
var orientation = change.NewValue.GetValueOrDefault<Orientation>(); var orientation = change.GetNewValue<Orientation>();
//Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation. //Note: For StackLayout Vertical Orientation means we have a Vertical ScrollOrientation.
//Horizontal Orientation means we have a Horizontal ScrollOrientation. //Horizontal Orientation means we have a Horizontal ScrollOrientation.

18
src/Avalonia.Base/Layout/UniformGridLayout.cs

@ -471,11 +471,11 @@ namespace Avalonia.Layout
gridState.ClearElementOnDataSourceChange(context, args); gridState.ClearElementOnDataSourceChange(context, args);
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
if (change.Property == OrientationProperty) if (change.Property == OrientationProperty)
{ {
var orientation = change.NewValue.GetValueOrDefault<Orientation>(); var orientation = change.GetNewValue<Orientation>();
//Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation. //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. //i.e. the properties are the inverse of each other.
@ -484,31 +484,31 @@ namespace Avalonia.Layout
} }
else if (change.Property == MinColumnSpacingProperty) else if (change.Property == MinColumnSpacingProperty)
{ {
_minColumnSpacing = change.NewValue.GetValueOrDefault<double>(); _minColumnSpacing = change.GetNewValue<double>();
} }
else if (change.Property == MinRowSpacingProperty) else if (change.Property == MinRowSpacingProperty)
{ {
_minRowSpacing = change.NewValue.GetValueOrDefault<double>(); _minRowSpacing = change.GetNewValue<double>();
} }
else if (change.Property == ItemsJustificationProperty) else if (change.Property == ItemsJustificationProperty)
{ {
_itemsJustification = change.NewValue.GetValueOrDefault<UniformGridLayoutItemsJustification>(); _itemsJustification = change.GetNewValue<UniformGridLayoutItemsJustification>();
} }
else if (change.Property == ItemsStretchProperty) else if (change.Property == ItemsStretchProperty)
{ {
_itemsStretch = change.NewValue.GetValueOrDefault<UniformGridLayoutItemsStretch>(); _itemsStretch = change.GetNewValue<UniformGridLayoutItemsStretch>();
} }
else if (change.Property == MinItemWidthProperty) else if (change.Property == MinItemWidthProperty)
{ {
_minItemWidth = change.NewValue.GetValueOrDefault<double>(); _minItemWidth = change.GetNewValue<double>();
} }
else if (change.Property == MinItemHeightProperty) else if (change.Property == MinItemHeightProperty)
{ {
_minItemHeight = change.NewValue.GetValueOrDefault<double>(); _minItemHeight = change.GetNewValue<double>();
} }
else if (change.Property == MaximumRowsOrColumnsProperty) else if (change.Property == MaximumRowsOrColumnsProperty)
{ {
_maximumRowsOrColumns = change.NewValue.GetValueOrDefault<int>(); _maximumRowsOrColumns = change.GetNewValue<int>();
} }
InvalidateLayout(); InvalidateLayout();

2
src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs

@ -322,7 +322,7 @@ namespace Avalonia.Layout
return finalSize; return finalSize;
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);

4
src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs

@ -8,7 +8,9 @@ namespace Avalonia.LogicalTree
/// </summary> /// </summary>
public class ChildIndexChangedEventArgs : EventArgs public class ChildIndexChangedEventArgs : EventArgs
{ {
public ChildIndexChangedEventArgs() public static new ChildIndexChangedEventArgs Empty { get; } = new ChildIndexChangedEventArgs();
private ChildIndexChangedEventArgs()
{ {
} }

83
src/Avalonia.Base/Media/Color.cs

@ -166,7 +166,10 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && // 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[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') && (s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') && (s[2] == 'b' || s[2] == 'B') &&
@ -175,7 +178,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') && (s[2] == 'l' || s[2] == 'L') &&
@ -185,7 +188,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') && (s[2] == 'v' || s[2] == 'V') &&
@ -229,7 +232,10 @@ namespace Avalonia.Media
// At this point all parsing uses strings // At this point all parsing uses strings
var str = s.ToString(); var str = s.ToString();
if (s.Length > 5 && // 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[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') && (s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') && (s[2] == 'b' || s[2] == 'B') &&
@ -238,7 +244,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') && (s[2] == 'l' || s[2] == 'L') &&
@ -248,7 +254,7 @@ namespace Avalonia.Media
return true; return true;
} }
if (s.Length > 5 && if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') && (s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') && (s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') && (s[2] == 'v' || s[2] == 'V') &&
@ -271,6 +277,9 @@ namespace Avalonia.Media
return false; return false;
} }
/// <summary>
/// Parses the given span of characters representing a hex color value into a new <see cref="Color"/>.
/// </summary>
private static bool TryParseHexFormat(ReadOnlySpan<char> s, out Color color) private static bool TryParseHexFormat(ReadOnlySpan<char> s, out Color color)
{ {
static bool TryParseCore(ReadOnlySpan<char> input, ref Color color) static bool TryParseCore(ReadOnlySpan<char> input, ref Color color)
@ -325,8 +334,13 @@ namespace Avalonia.Media
return TryParseCore(input, ref color); return TryParseCore(input, ref color);
} }
/// <summary>
/// Parses the given string representing a CSS color value into a new <see cref="Color"/>.
/// </summary>
private static bool TryParseCssFormat(string s, out Color color) private static bool TryParseCssFormat(string s, out Color color)
{ {
bool prefixMatched = false;
color = default; color = default;
if (s is null) if (s is null)
@ -342,27 +356,35 @@ namespace Avalonia.Media
return false; return false;
} }
if (workingString.Length > 6 && if (workingString.Length >= 11 &&
workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) && workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(5, workingString.Length - 6); workingString = workingString.Substring(5, workingString.Length - 6);
prefixMatched = true;
} }
if (workingString.Length > 5 && if (prefixMatched == false &&
workingString.Length >= 10 &&
workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) && workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(4, workingString.Length - 5); workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
} }
string[] components = workingString.Split(','); string[] components = workingString.Split(',');
if (components.Length == 3) // RGB if (components.Length == 3) // RGB
{ {
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) && if (InternalTryParseByte(components[0], out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) && InternalTryParseByte(components[1], out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue)) InternalTryParseByte(components[2], out byte blue))
{ {
color = new Color(0xFF, red, green, blue); color = new Color(0xFF, red, green, blue);
return true; return true;
@ -370,18 +392,45 @@ namespace Avalonia.Media
} }
else if (components.Length == 4) // RGBA else if (components.Length == 4) // RGBA
{ {
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) && if (InternalTryParseByte(components[0], out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) && InternalTryParseByte(components[1], out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue) && InternalTryParseByte(components[2], out byte blue) &&
TryInternalParse(components[3], out double alpha)) InternalTryParseDouble(components[3], out double alpha))
{ {
color = new Color((byte)(alpha * 255), red, green, blue); color = new Color((byte)Math.Round(alpha * 255.0), red, green, blue);
return true; 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 // Local function to specially parse a double value with an optional percentage sign
bool TryInternalParse(string inString, out double outDouble) bool InternalTryParseDouble(string inString, out double outDouble)
{ {
// The percent sign, if it exists, must be at the end of the number // The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);

5
src/Avalonia.Base/Media/DashStyle.cs

@ -112,14 +112,13 @@ namespace Avalonia.Media
/// <returns></returns> /// <returns></returns>
public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset); public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset);
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == DashesProperty) if (change.Property == DashesProperty)
{ {
var oldValue = change.OldValue.GetValueOrDefault<AvaloniaList<double>>(); var (oldValue, newValue) = change.GetOldAndNewValue<AvaloniaList<double>>();
var newValue = change.NewValue.GetValueOrDefault<AvaloniaList<double>>();
if (oldValue is object) if (oldValue is object)
{ {

2
src/Avalonia.Base/Media/DrawingImage.cs

@ -69,7 +69,7 @@ namespace Avalonia.Media
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);

55
src/Avalonia.Base/Media/HslColor.cs

@ -12,6 +12,7 @@ namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Defines a color using the hue/saturation/lightness (HSL) model. /// Defines a color using the hue/saturation/lightness (HSL) model.
/// This uses a cylindrical-coordinate representation of a color.
/// </summary> /// </summary>
#if !BUILDTASK #if !BUILDTASK
public public
@ -98,24 +99,53 @@ namespace Avalonia.Media
} }
/// <summary> /// <summary>
/// Gets the Alpha (transparency) component in the range from 0..1. /// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully transparent.</item>
/// <item>1 is fully opaque.</item>
/// </list>
/// </remarks>
public double A { get; } public double A { get; }
/// <summary> /// <summary>
/// Gets the Hue component in the range from 0..360. /// 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. /// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0/360 degrees is Red.</item>
/// <item>60 degrees is Yellow.</item>
/// <item>120 degrees is Green.</item>
/// <item>180 degrees is Cyan.</item>
/// <item>240 degrees is Blue.</item>
/// <item>300 degrees is Magenta.</item>
/// </list>
/// </remarks>
public double H { get; } public double H { get; }
/// <summary> /// <summary>
/// Gets the Saturation component in the range from 0..1. /// Gets the Saturation component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is a shade of gray (no color).</item>
/// <item>1 is the full color.</item>
/// </list>
/// </remarks>
public double S { get; } public double S { get; }
/// <summary> /// <summary>
/// Gets the Lightness component in the range from 0..1. /// Gets the Lightness component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully black.</item>
/// <item>1 is fully white.</item>
/// </list>
/// </remarks>
public double L { get; } public double L { get; }
/// <inheritdoc/> /// <inheritdoc/>
@ -226,6 +256,8 @@ namespace Avalonia.Media
/// <returns>True if parsing was successful; otherwise, false.</returns> /// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HslColor hslColor) public static bool TryParse(string s, out HslColor hslColor)
{ {
bool prefixMatched = false;
hslColor = default; hslColor = default;
if (s is null) if (s is null)
@ -241,18 +273,29 @@ namespace Avalonia.Media
return false; return false;
} }
if (workingString.Length > 6 && // 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.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(5, workingString.Length - 6); workingString = workingString.Substring(5, workingString.Length - 6);
prefixMatched = true;
} }
if (workingString.Length > 5 && if (prefixMatched == false &&
workingString.Length >= 10 &&
workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) && workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(4, workingString.Length - 5); workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
} }
string[] components = workingString.Split(','); string[] components = workingString.Split(',');

55
src/Avalonia.Base/Media/HsvColor.cs

@ -12,6 +12,7 @@ namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Defines a color using the hue/saturation/value (HSV) model. /// Defines a color using the hue/saturation/value (HSV) model.
/// This uses a cylindrical-coordinate representation of a color.
/// </summary> /// </summary>
#if !BUILDTASK #if !BUILDTASK
public public
@ -98,24 +99,53 @@ namespace Avalonia.Media
} }
/// <summary> /// <summary>
/// Gets the Alpha (transparency) component in the range from 0..1. /// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully transparent.</item>
/// <item>1 is fully opaque.</item>
/// </list>
/// </remarks>
public double A { get; } public double A { get; }
/// <summary> /// <summary>
/// Gets the Hue component in the range from 0..360. /// 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. /// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0/360 degrees is Red.</item>
/// <item>60 degrees is Yellow.</item>
/// <item>120 degrees is Green.</item>
/// <item>180 degrees is Cyan.</item>
/// <item>240 degrees is Blue.</item>
/// <item>300 degrees is Magenta.</item>
/// </list>
/// </remarks>
public double H { get; } public double H { get; }
/// <summary> /// <summary>
/// Gets the Saturation component in the range from 0..1. /// Gets the Saturation component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is a shade of gray (no color).</item>
/// <item>1 is the full color.</item>
/// </list>
/// </remarks>
public double S { get; } public double S { get; }
/// <summary> /// <summary>
/// Gets the Value component in the range from 0..1. /// Gets the Value (or Brightness/Intensity) component in the range from 0..1 (percentage).
/// </summary> /// </summary>
/// <remarks>
/// <list type="bullet">
/// <item>0 is fully black and shows no color.</item>
/// <item>1 is the brightest and shows full color.</item>
/// </list>
/// </remarks>
public double V { get; } public double V { get; }
/// <inheritdoc/> /// <inheritdoc/>
@ -226,6 +256,8 @@ namespace Avalonia.Media
/// <returns>True if parsing was successful; otherwise, false.</returns> /// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HsvColor hsvColor) public static bool TryParse(string s, out HsvColor hsvColor)
{ {
bool prefixMatched = false;
hsvColor = default; hsvColor = default;
if (s is null) if (s is null)
@ -241,18 +273,29 @@ namespace Avalonia.Media
return false; return false;
} }
if (workingString.Length > 6 && // 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.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(5, workingString.Length - 6); workingString = workingString.Substring(5, workingString.Length - 6);
prefixMatched = true;
} }
if (workingString.Length > 5 && if (prefixMatched == false &&
workingString.Length >= 10 &&
workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) && workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal)) workingString.EndsWith(")", StringComparison.Ordinal))
{ {
workingString = workingString.Substring(4, workingString.Length - 5); workingString = workingString.Substring(4, workingString.Length - 5);
prefixMatched = true;
}
if (prefixMatched == false)
{
return false;
} }
string[] components = workingString.Split(','); string[] components = workingString.Split(',');

28
src/Avalonia.Base/Media/Pen.cs

@ -7,7 +7,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Describes how a stroke is drawn. /// Describes how a stroke is drawn.
/// </summary> /// </summary>
public sealed class Pen : AvaloniaObject, IPen, IWeakEventSubscriber<EventArgs> public sealed class Pen : AvaloniaObject, IPen
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Brush"/> property. /// Defines the <see cref="Brush"/> property.
@ -48,7 +48,8 @@ namespace Avalonia.Media
private EventHandler? _invalidated; private EventHandler? _invalidated;
private IAffectsRender? _subscribedToBrush; private IAffectsRender? _subscribedToBrush;
private IAffectsRender? _subscribedToDashes; private IAffectsRender? _subscribedToDashes;
private TargetWeakEventSubscriber<Pen, EventArgs>? _weakSubscriber;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class. /// Initializes a new instance of the <see cref="Pen"/> class.
/// </summary> /// </summary>
@ -192,7 +193,7 @@ namespace Avalonia.Media
MiterLimit); MiterLimit);
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
_invalidated?.Invoke(this, EventArgs.Empty); _invalidated?.Invoke(this, EventArgs.Empty);
if(change.Property == BrushProperty) if(change.Property == BrushProperty)
@ -207,13 +208,24 @@ namespace Avalonia.Media
{ {
if ((_invalidated == null || field != value) && field != null) if ((_invalidated == null || field != value) && field != null)
{ {
InvalidatedWeakEvent.Unsubscribe(field, this); if (_weakSubscriber != null)
InvalidatedWeakEvent.Unsubscribe(field, _weakSubscriber);
field = null; field = null;
} }
if (_invalidated != null && field != value && value is IAffectsRender affectsRender) if (_invalidated != null && field != value && value is IAffectsRender affectsRender)
{ {
InvalidatedWeakEvent.Subscribe(affectsRender, this); if (_weakSubscriber == null)
{
_weakSubscriber = new TargetWeakEventSubscriber<Pen, EventArgs>(
this, static (target, _, ev, _) =>
{
if (ev == InvalidatedWeakEvent)
target._invalidated?.Invoke(target, EventArgs.Empty);
});
}
InvalidatedWeakEvent.Subscribe(affectsRender, _weakSubscriber);
field = affectsRender; field = affectsRender;
} }
} }
@ -223,11 +235,5 @@ namespace Avalonia.Media
UpdateSubscription(ref _subscribedToBrush, Brush); UpdateSubscription(ref _subscribedToBrush, Brush);
UpdateSubscription(ref _subscribedToDashes, DashStyle); UpdateSubscription(ref _subscribedToDashes, DashStyle);
} }
void IWeakEventSubscriber<EventArgs>.OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
if (ev == InvalidatedWeakEvent)
_invalidated?.Invoke(this, EventArgs.Empty);
}
} }
} }

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

@ -21,6 +21,7 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, 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.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]

17
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -18,19 +18,19 @@ namespace Avalonia.PropertyStore
/// <typeparam name="T">The property type.</typeparam> /// <typeparam name="T">The property type.</typeparam>
internal class BindingEntry<T> : IBindingEntry, IPriorityValueEntry<T>, IObserver<BindingValue<T>> internal class BindingEntry<T> : IBindingEntry, IPriorityValueEntry<T>, IObserver<BindingValue<T>>
{ {
private readonly IAvaloniaObject _owner; private readonly AvaloniaObject _owner;
private IValueSink _sink; private ValueOwner<T> _sink;
private IDisposable? _subscription; private IDisposable? _subscription;
private bool _isSubscribed; private bool _isSubscribed;
private bool _batchUpdate; private bool _batchUpdate;
private Optional<T> _value; private Optional<T> _value;
public BindingEntry( public BindingEntry(
IAvaloniaObject owner, AvaloniaObject owner,
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source, IObservable<BindingValue<T>> source,
BindingPriority priority, BindingPriority priority,
IValueSink sink) ValueOwner<T> sink)
{ {
_owner = owner; _owner = owner;
Property = property; Property = property;
@ -50,7 +50,7 @@ namespace Avalonia.PropertyStore
{ {
_batchUpdate = false; _batchUpdate = false;
if (_sink is ValueStore) if (_sink.IsValueStore)
Start(); Start();
} }
@ -113,16 +113,15 @@ namespace Avalonia.PropertyStore
} }
} }
public void Reparent(IValueSink sink) => _sink = sink; public void Reparent(PriorityValue<T> parent) => _sink = new(parent);
public void RaiseValueChanged( public void RaiseValueChanged(
IValueSink sink, AvaloniaObject owner,
IAvaloniaObject owner,
AvaloniaProperty property, AvaloniaProperty property,
Optional<object?> oldValue, Optional<object?> oldValue,
Optional<object?> newValue) Optional<object?> newValue)
{ {
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner, owner,
(AvaloniaProperty<T>)property, (AvaloniaProperty<T>)property,
oldValue.Cast<T>(), oldValue.Cast<T>(),

13
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -18,14 +18,14 @@ namespace Avalonia.PropertyStore
/// <typeparam name="T">The property type.</typeparam> /// <typeparam name="T">The property type.</typeparam>
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry
{ {
private IValueSink _sink; private ValueOwner<T> _sink;
private Optional<T> _value; private Optional<T> _value;
public ConstantValueEntry( public ConstantValueEntry(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
T value, T value,
BindingPriority priority, BindingPriority priority,
IValueSink sink) ValueOwner<T> sink)
{ {
Property = property; Property = property;
_value = value; _value = value;
@ -37,7 +37,7 @@ namespace Avalonia.PropertyStore
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
Optional<T> value, Optional<T> value,
BindingPriority priority, BindingPriority priority,
IValueSink sink) ValueOwner<T> sink)
{ {
Property = property; Property = property;
_value = value; _value = value;
@ -62,17 +62,16 @@ namespace Avalonia.PropertyStore
_sink.Completed(Property, this, oldValue); _sink.Completed(Property, this, oldValue);
} }
public void Reparent(IValueSink sink) => _sink = sink; public void Reparent(PriorityValue<T> sink) => _sink = new(sink);
public void Start() { } public void Start() { }
public void RaiseValueChanged( public void RaiseValueChanged(
IValueSink sink, AvaloniaObject owner,
IAvaloniaObject owner,
AvaloniaProperty property, AvaloniaProperty property,
Optional<object?> oldValue, Optional<object?> oldValue,
Optional<object?> newValue) Optional<object?> newValue)
{ {
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner, owner,
(AvaloniaProperty<T>)property, (AvaloniaProperty<T>)property,
oldValue.Cast<T>(), oldValue.Cast<T>(),

2
src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs

@ -5,7 +5,6 @@
/// </summary> /// </summary>
internal interface IPriorityValueEntry : IValue internal interface IPriorityValueEntry : IValue
{ {
void Reparent(IValueSink sink);
} }
/// <summary> /// <summary>
@ -14,5 +13,6 @@
/// <typeparam name="T">The property type.</typeparam> /// <typeparam name="T">The property type.</typeparam>
internal interface IPriorityValueEntry<T> : IPriorityValueEntry, IValue<T> internal interface IPriorityValueEntry<T> : IPriorityValueEntry, IValue<T>
{ {
void Reparent(PriorityValue<T> parent);
} }
} }

3
src/Avalonia.Base/PropertyStore/IValue.cs

@ -11,8 +11,7 @@ namespace Avalonia.PropertyStore
Optional<object?> GetValue(); Optional<object?> GetValue();
void Start(); void Start();
void RaiseValueChanged( void RaiseValueChanged(
IValueSink sink, AvaloniaObject owner,
IAvaloniaObject owner,
AvaloniaProperty property, AvaloniaProperty property,
Optional<object?> oldValue, Optional<object?> oldValue,
Optional<object?> newValue); Optional<object?> newValue);

17
src/Avalonia.Base/PropertyStore/IValueSink.cs

@ -1,17 +0,0 @@
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an entity that can receive change notifications in a <see cref="ValueStore"/>.
/// </summary>
internal interface IValueSink
{
void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change);
void Completed<T>(
StyledPropertyBase<T> property,
IPriorityValueEntry entry,
Optional<T> oldValue);
}
}

5
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@ -25,13 +25,12 @@ namespace Avalonia.PropertyStore
public void Start() { } public void Start() { }
public void RaiseValueChanged( public void RaiseValueChanged(
IValueSink sink, AvaloniaObject owner,
IAvaloniaObject owner,
AvaloniaProperty property, AvaloniaProperty property,
Optional<object?> oldValue, Optional<object?> oldValue,
Optional<object?> newValue) Optional<object?> newValue)
{ {
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner, owner,
(AvaloniaProperty<T>)property, (AvaloniaProperty<T>)property,
oldValue.Cast<T>(), oldValue.Cast<T>(),

67
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -1,10 +1,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Data; using Avalonia.Data;
namespace Avalonia.PropertyStore namespace Avalonia.PropertyStore
{ {
/// <summary>
/// Represents an untyped interface to <see cref="PriorityValue{T}"/>.
/// </summary>
interface IPriorityValue : IValue
{
void UpdateEffectiveValue();
}
/// <summary> /// <summary>
/// Stores a set of prioritized values and bindings in a <see cref="ValueStore"/>. /// Stores a set of prioritized values and bindings in a <see cref="ValueStore"/>.
/// </summary> /// </summary>
@ -16,10 +23,10 @@ namespace Avalonia.PropertyStore
/// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order /// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order
/// they were added) plus a local value. /// they were added) plus a local value.
/// </remarks> /// </remarks>
internal class PriorityValue<T> : IValue<T>, IValueSink, IBatchUpdate internal class PriorityValue<T> : IPriorityValue, IValue<T>, IBatchUpdate
{ {
private readonly IAvaloniaObject _owner; private readonly AvaloniaObject _owner;
private readonly IValueSink _sink; private readonly ValueStore _store;
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>(); private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
private readonly Func<IAvaloniaObject, T, T>? _coerceValue; private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
private Optional<T> _localValue; private Optional<T> _localValue;
@ -28,13 +35,13 @@ namespace Avalonia.PropertyStore
private bool _batchUpdate; private bool _batchUpdate;
public PriorityValue( public PriorityValue(
IAvaloniaObject owner, AvaloniaObject owner,
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
IValueSink sink) ValueStore store)
{ {
_owner = owner; _owner = owner;
Property = property; Property = property;
_sink = sink; _store = store;
if (property.HasCoercion) if (property.HasCoercion)
{ {
@ -44,11 +51,11 @@ namespace Avalonia.PropertyStore
} }
public PriorityValue( public PriorityValue(
IAvaloniaObject owner, AvaloniaObject owner,
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
IValueSink sink, ValueStore store,
IPriorityValueEntry<T> existing) IPriorityValueEntry<T> existing)
: this(owner, property, sink) : this(owner, property, store)
{ {
existing.Reparent(this); existing.Reparent(this);
_entries.Add(existing); _entries.Add(existing);
@ -75,9 +82,9 @@ namespace Avalonia.PropertyStore
} }
public PriorityValue( public PriorityValue(
IAvaloniaObject owner, AvaloniaObject owner,
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
IValueSink sink, ValueStore sink,
LocalValueEntry<T> existing) LocalValueEntry<T> existing)
: this(owner, property, sink) : this(owner, property, sink)
{ {
@ -148,7 +155,7 @@ namespace Avalonia.PropertyStore
else else
{ {
var insert = FindInsertPoint(priority); var insert = FindInsertPoint(priority);
var entry = new ConstantValueEntry<T>(Property, value, priority, this); var entry = new ConstantValueEntry<T>(Property, value, priority, new ValueOwner<T>(this));
_entries.Insert(insert, entry); _entries.Insert(insert, entry);
result = entry; result = entry;
} }
@ -165,7 +172,7 @@ namespace Avalonia.PropertyStore
public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority) public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority)
{ {
var binding = new BindingEntry<T>(_owner, Property, source, priority, this); var binding = new BindingEntry<T>(_owner, Property, source, priority, new(this));
var insert = FindInsertPoint(binding.Priority); var insert = FindInsertPoint(binding.Priority);
_entries.Insert(insert, binding); _entries.Insert(insert, binding);
@ -186,13 +193,12 @@ namespace Avalonia.PropertyStore
public void Start() => UpdateEffectiveValue(null); public void Start() => UpdateEffectiveValue(null);
public void RaiseValueChanged( public void RaiseValueChanged(
IValueSink sink, AvaloniaObject owner,
IAvaloniaObject owner,
AvaloniaProperty property, AvaloniaProperty property,
Optional<object?> oldValue, Optional<object?> oldValue,
Optional<object?> newValue) Optional<object?> newValue)
{ {
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner, owner,
(AvaloniaProperty<T>)property, (AvaloniaProperty<T>)property,
oldValue.Cast<T>(), oldValue.Cast<T>(),
@ -200,7 +206,7 @@ namespace Avalonia.PropertyStore
Priority)); Priority));
} }
void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change) public void ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
{ {
if (change.Priority == BindingPriority.LocalValue) if (change.Priority == BindingPriority.LocalValue)
{ {
@ -213,22 +219,15 @@ namespace Avalonia.PropertyStore
} }
} }
void IValueSink.Completed<TValue>( public void Completed(IPriorityValueEntry entry, Optional<T> oldValue)
StyledPropertyBase<TValue> property,
IPriorityValueEntry entry,
Optional<TValue> oldValue)
{ {
_entries.Remove((IPriorityValueEntry<T>)entry); _entries.Remove((IPriorityValueEntry<T>)entry);
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
if (oldValue is Optional<T> o) _owner,
{ Property,
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>( oldValue,
_owner, default,
Property, entry.Priority));
o,
default,
entry.Priority));
}
} }
private int FindInsertPoint(BindingPriority priority) private int FindInsertPoint(BindingPriority priority)
@ -308,7 +307,7 @@ namespace Avalonia.PropertyStore
var old = _value; var old = _value;
_value = value; _value = value;
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( _store.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner, _owner,
Property, Property,
old, old,
@ -319,7 +318,7 @@ namespace Avalonia.PropertyStore
{ {
change.MarkNonEffectiveValue(); change.MarkNonEffectiveValue();
change.SetOldValue(default); change.SetOldValue(default);
_sink.ValueChanged(change); _store.ValueChanged(change);
} }
} }
} }

45
src/Avalonia.Base/PropertyStore/ValueOwner.cs

@ -0,0 +1,45 @@
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents a union type of <see cref="ValueStore"/> and <see cref="PriorityValue{T}"/>,
/// which are the valid owners of a value store <see cref="IValue"/>.
/// </summary>
/// <typeparam name="T">The value type.</typeparam>
internal readonly struct ValueOwner<T>
{
private readonly ValueStore? _store;
private readonly PriorityValue<T>? _priorityValue;
public ValueOwner(ValueStore o)
{
_store = o;
_priorityValue = null;
}
public ValueOwner(PriorityValue<T> v)
{
_store = null;
_priorityValue = v;
}
public bool IsValueStore => _store is not null;
public void Completed(StyledPropertyBase<T> property, IPriorityValueEntry entry, Optional<T> oldValue)
{
if (_store is not null)
_store?.Completed(property, entry, oldValue);
else
_priorityValue!.Completed(entry, oldValue);
}
public void ValueChanged(AvaloniaPropertyChangedEventArgs<T> e)
{
if (_store is not null)
_store?.ValueChanged(e);
else
_priorityValue!.ValueChanged(e);
}
}
}

2
src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs

@ -51,7 +51,7 @@ namespace Avalonia.Reactive
{ {
if (e is AvaloniaPropertyChangedEventArgs<T> typedArgs) if (e is AvaloniaPropertyChangedEventArgs<T> typedArgs)
{ {
var newValue = e.Sender.GetValue(typedArgs.Property); var newValue = e.Sender.GetValue<T>(typedArgs.Property);
if (!_value.HasValue || !EqualityComparer<T>.Default.Equals(newValue, _value.Value)) if (!_value.HasValue || !EqualityComparer<T>.Default.Equals(newValue, _value.Value))
{ {

30
src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs

@ -49,23 +49,31 @@ namespace Avalonia.Reactive
{ {
if (e.Property == _property) if (e.Property == _property)
{ {
T newValue; if (e.Sender is AvaloniaObject ao)
if (e is AvaloniaPropertyChangedEventArgs<T> typed)
{ {
newValue = typed.Sender.GetValue(typed.Property); T newValue;
if (e is AvaloniaPropertyChangedEventArgs<T> typed)
{
newValue = AvaloniaObjectExtensions.GetValue(ao, typed.Property);
}
else
{
newValue = (T)e.Sender.GetValue(e.Property)!;
}
if (!_value.HasValue ||
!EqualityComparer<T>.Default.Equals(newValue, _value.Value))
{
_value = newValue;
PublishNext(_value.Value!);
}
} }
else else
{ {
newValue = (T)e.Sender.GetValue(e.Property)!; throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
} }
if (!_value.HasValue ||
!EqualityComparer<T>.Default.Equals(newValue, _value.Value))
{
_value = newValue;
PublishNext(_value.Value!);
}
} }
} }
} }

15
src/Avalonia.Base/RelativePoint.cs

@ -1,7 +1,8 @@
using System; using System;
using System.Globalization; using System.Globalization;
#if !BUILDTASK
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
#endif
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
@ -10,7 +11,10 @@ namespace Avalonia
/// Defines the reference point units of an <see cref="RelativePoint"/> or /// Defines the reference point units of an <see cref="RelativePoint"/> or
/// <see cref="RelativeRect"/>. /// <see cref="RelativeRect"/>.
/// </summary> /// </summary>
public enum RelativeUnit #if !BUILDTASK
public
#endif
enum RelativeUnit
{ {
/// <summary> /// <summary>
/// The point is expressed as a fraction of the containing element's size. /// The point is expressed as a fraction of the containing element's size.
@ -26,7 +30,10 @@ namespace Avalonia
/// <summary> /// <summary>
/// Defines a point that may be defined relative to a containing element. /// Defines a point that may be defined relative to a containing element.
/// </summary> /// </summary>
public readonly struct RelativePoint : IEquatable<RelativePoint> #if !BUILDTASK
public
#endif
readonly struct RelativePoint : IEquatable<RelativePoint>
{ {
/// <summary> /// <summary>
/// A point at the top left of the containing element. /// A point at the top left of the containing element.
@ -49,7 +56,9 @@ namespace Avalonia
static RelativePoint() static RelativePoint()
{ {
#if !BUILDTASK
Animation.Animation.RegisterAnimator<RelativePointAnimator>(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType)); Animation.Animation.RegisterAnimator<RelativePointAnimator>(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType));
#endif
} }
/// <summary> /// <summary>

7
src/Avalonia.Base/StyledElement.cs

@ -467,7 +467,12 @@ namespace Avalonia
/// <param name="parent">The parent.</param> /// <param name="parent">The parent.</param>
void ISetInheritanceParent.SetParent(IAvaloniaObject? parent) void ISetInheritanceParent.SetParent(IAvaloniaObject? parent)
{ {
InheritanceParent = parent; InheritanceParent = parent switch
{
AvaloniaObject ao => ao,
null => null,
_ => throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.")
};
} }
void IStyleable.StyleApplied(IStyleInstance instance) void IStyleable.StyleApplied(IStyleInstance instance)

44
src/Avalonia.Base/StyledPropertyBase.cs

@ -2,6 +2,7 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Styling;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia namespace Avalonia
@ -158,12 +159,6 @@ namespace Avalonia
base.OverrideMetadata(type, metadata); base.OverrideMetadata(type, metadata);
} }
/// <inheritdoc/>
public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> visitor, ref TData data)
{
visitor.Visit(this, ref data);
}
/// <summary> /// <summary>
/// Gets the string representation of the property. /// Gets the string representation of the property.
/// </summary> /// </summary>
@ -177,19 +172,19 @@ namespace Avalonia
object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteClearValue(IAvaloniaObject o) internal override void RouteClearValue(AvaloniaObject o)
{ {
o.ClearValue<TValue>(this); o.ClearValue<TValue>(this);
} }
/// <inheritdoc/> /// <inheritdoc/>
internal override object? RouteGetValue(IAvaloniaObject o) internal override object? RouteGetValue(AvaloniaObject o)
{ {
return o.GetValue<TValue>(this); return o.GetValue<TValue>(this);
} }
/// <inheritdoc/> /// <inheritdoc/>
internal override object? RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) internal override object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority)
{ {
var value = o.GetBaseValue<TValue>(this, maxPriority); var value = o.GetBaseValue<TValue>(this, maxPriority);
return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue;
@ -197,7 +192,7 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
internal override IDisposable? RouteSetValue( internal override IDisposable? RouteSetValue(
IAvaloniaObject o, AvaloniaObject o,
object? value, object? value,
BindingPriority priority) BindingPriority priority)
{ {
@ -221,7 +216,7 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
internal override IDisposable RouteBind( internal override IDisposable RouteBind(
IAvaloniaObject o, AvaloniaObject o,
IObservable<BindingValue<object?>> source, IObservable<BindingValue<object?>> source,
BindingPriority priority) BindingPriority priority)
{ {
@ -232,11 +227,36 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
internal override void RouteInheritanceParentChanged( internal override void RouteInheritanceParentChanged(
AvaloniaObject o, AvaloniaObject o,
IAvaloniaObject? oldParent) AvaloniaObject? oldParent)
{ {
o.InheritanceParentChanged(this, oldParent); o.InheritanceParentChanged(this, oldParent);
} }
internal override ISetterInstance CreateSetterInstance(IStyleable target, object? value)
{
if (value is IBinding binding)
{
return new PropertySetterBindingInstance<TValue>(
target,
this,
binding);
}
else if (value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(PropertyType))
{
return new PropertySetterLazyInstance<TValue>(
target,
this,
() => (TValue)template.Build());
}
else
{
return new PropertySetterInstance<TValue>(
target,
this,
(TValue)value!);
}
}
private object? GetDefaultBoxedValue(Type type) private object? GetDefaultBoxedValue(Type type)
{ {
_ = type ?? throw new ArgumentNullException(nameof(type)); _ = type ?? throw new ArgumentNullException(nameof(type));

65
src/Avalonia.Base/Styling/Setter.cs

@ -16,7 +16,7 @@ namespace Avalonia.Styling
/// A <see cref="Setter"/> is used to set a <see cref="AvaloniaProperty"/> value on a /// A <see cref="Setter"/> is used to set a <see cref="AvaloniaProperty"/> value on a
/// <see cref="AvaloniaObject"/> depending on a condition. /// <see cref="AvaloniaObject"/> depending on a condition.
/// </remarks> /// </remarks>
public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor<Setter.SetterVisitorData> public class Setter : ISetter, IAnimationSetter
{ {
private object? _value; private object? _value;
@ -68,68 +68,7 @@ namespace Avalonia.Styling
throw new InvalidOperationException("Setter.Property must be set."); throw new InvalidOperationException("Setter.Property must be set.");
} }
var data = new SetterVisitorData return Property.CreateSetterInstance(target, Value);
{
target = target,
value = Value,
};
Property.Accept(this, ref data);
return data.result!;
}
void IAvaloniaPropertyVisitor<SetterVisitorData>.Visit<T>(
StyledPropertyBase<T> property,
ref SetterVisitorData data)
{
if (data.value is IBinding binding)
{
data.result = new PropertySetterBindingInstance<T>(
data.target,
property,
binding);
}
else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
{
data.result = new PropertySetterLazyInstance<T>(
data.target,
property,
() => (T)template.Build());
}
else
{
data.result = new PropertySetterInstance<T>(
data.target,
property,
(T)data.value!);
}
}
void IAvaloniaPropertyVisitor<SetterVisitorData>.Visit<T>(
DirectPropertyBase<T> property,
ref SetterVisitorData data)
{
if (data.value is IBinding binding)
{
data.result = new PropertySetterBindingInstance<T>(
data.target,
property,
binding);
}
else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
{
data.result = new PropertySetterLazyInstance<T>(
data.target,
property,
() => (T)template.Build());
}
else
{
data.result = new PropertySetterInstance<T>(
data.target,
property,
(T)data.value!);
}
} }
private struct SetterVisitorData private struct SetterVisitorData

2
src/Avalonia.Base/Threading/AvaloniaScheduler.cs

@ -46,7 +46,7 @@ namespace Avalonia.Threading
{ {
composite.Add(action(this, state)); composite.Add(action(this, state));
} }
}, DispatcherPriority.DataBind); }, DispatcherPriority.Background);
composite.Add(cancellation); composite.Add(cancellation);

12
src/Avalonia.Base/Threading/Dispatcher.cs

@ -83,42 +83,42 @@ namespace Avalonia.Threading
_jobRunner.HasJobsWithPriority(minimumPriority); _jobRunner.HasJobsWithPriority(minimumPriority);
/// <inheritdoc/> /// <inheritdoc/>
public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) public Task InvokeAsync(Action action, DispatcherPriority priority = default)
{ {
_ = action ?? throw new ArgumentNullException(nameof(action)); _ = action ?? throw new ArgumentNullException(nameof(action));
return _jobRunner.InvokeAsync(action, priority); return _jobRunner.InvokeAsync(action, priority);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority = DispatcherPriority.Normal) public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority = default)
{ {
_ = function ?? throw new ArgumentNullException(nameof(function)); _ = function ?? throw new ArgumentNullException(nameof(function));
return _jobRunner.InvokeAsync(function, priority); return _jobRunner.InvokeAsync(function, priority);
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task InvokeAsync(Func<Task> function, DispatcherPriority priority = DispatcherPriority.Normal) public Task InvokeAsync(Func<Task> function, DispatcherPriority priority = default)
{ {
_ = function ?? throw new ArgumentNullException(nameof(function)); _ = function ?? throw new ArgumentNullException(nameof(function));
return _jobRunner.InvokeAsync(function, priority).Unwrap(); return _jobRunner.InvokeAsync(function, priority).Unwrap();
} }
/// <inheritdoc/> /// <inheritdoc/>
public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function, DispatcherPriority priority = DispatcherPriority.Normal) public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function, DispatcherPriority priority = default)
{ {
_ = function ?? throw new ArgumentNullException(nameof(function)); _ = function ?? throw new ArgumentNullException(nameof(function));
return _jobRunner.InvokeAsync(function, priority).Unwrap(); return _jobRunner.InvokeAsync(function, priority).Unwrap();
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) public void Post(Action action, DispatcherPriority priority = default)
{ {
_ = action ?? throw new ArgumentNullException(nameof(action)); _ = action ?? throw new ArgumentNullException(nameof(action));
_jobRunner.Post(action, priority); _jobRunner.Post(action, priority);
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Post<T>(Action<T> action, T arg, DispatcherPriority priority = DispatcherPriority.Normal) public void Post<T>(Action<T> action, T arg, DispatcherPriority priority = default)
{ {
_ = action ?? throw new ArgumentNullException(nameof(action)); _ = action ?? throw new ArgumentNullException(nameof(action));
_jobRunner.Post(action, arg, priority); _jobRunner.Post(action, arg, priority);

97
src/Avalonia.Base/Threading/DispatcherPriority.cs

@ -1,74 +1,121 @@
using System;
namespace Avalonia.Threading namespace Avalonia.Threading
{ {
/// <summary> /// <summary>
/// Defines the priorities with which jobs can be invoked on a <see cref="Dispatcher"/>. /// Defines the priorities with which jobs can be invoked on a <see cref="Dispatcher"/>.
/// </summary> /// </summary>
// TODO: These are copied from WPF - many won't apply to Avalonia. public readonly struct DispatcherPriority : IEquatable<DispatcherPriority>, IComparable<DispatcherPriority>
public enum DispatcherPriority
{ {
/// <summary>
/// The integer value of the priority
/// </summary>
public int Value { get; }
private DispatcherPriority(int value)
{
Value = value;
}
/// <summary> /// <summary>
/// Minimum possible priority /// Minimum possible priority
/// </summary> /// </summary>
MinValue = 1, public static readonly DispatcherPriority MinValue = new(0);
/// <summary> /// <summary>
/// The job will be processed when the system is idle. /// The job will be processed when the system is idle.
/// </summary> /// </summary>
SystemIdle = 1, [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinValue;
/// <summary> /// <summary>
/// The job will be processed when the application is idle. /// The job will be processed when the application is idle.
/// </summary> /// </summary>
ApplicationIdle = 2, [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = MinValue;
/// <summary> /// <summary>
/// The job will be processed after background operations have completed. /// The job will be processed after background operations have completed.
/// </summary> /// </summary>
ContextIdle = 3, [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = MinValue;
/// <summary> /// <summary>
/// The job will be processed after other non-idle operations have completed. /// The job will be processed with normal priority.
/// </summary> /// </summary>
Background = 4, public static readonly DispatcherPriority Normal = MinValue;
/// <summary> /// <summary>
/// The job will be processed with the same priority as input. /// The job will be processed after other non-idle operations have completed.
/// </summary> /// </summary>
Input = 5, public static readonly DispatcherPriority Background = new(1);
/// <summary> /// <summary>
/// The job will be processed after layout and render but before input. /// The job will be processed with the same priority as input.
/// </summary> /// </summary>
Loaded = 6, public static readonly DispatcherPriority Input = new(2);
/// <summary> /// <summary>
/// The job will be processed with the same priority as render. /// The job will be processed after layout and render but before input.
/// </summary> /// </summary>
Render = 7, public static readonly DispatcherPriority Loaded = new(3);
/// <summary> /// <summary>
/// The job will be processed with the same priority as render. /// The job will be processed with the same priority as render.
/// </summary> /// </summary>
Layout = 8, public static readonly DispatcherPriority Render = new(5);
/// <summary> /// <summary>
/// The job will be processed with the same priority as data binding. /// The job will be processed with the same priority as render.
/// </summary> /// </summary>
DataBind = 9, public static readonly DispatcherPriority Layout = new(6);
/// <summary> /// <summary>
/// The job will be processed with normal priority. /// The job will be processed with the same priority as data binding.
/// </summary> /// </summary>
Normal = 10, [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = MinValue;
/// <summary> /// <summary>
/// The job will be processed before other asynchronous operations. /// The job will be processed before other asynchronous operations.
/// </summary> /// </summary>
Send = 11, public static readonly DispatcherPriority Send = new(7);
/// <summary> /// <summary>
/// Maximum possible priority /// Maximum possible priority
/// </summary> /// </summary>
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);
/// <inheritdoc />
public bool Equals(DispatcherPriority other) => Value == other.Value;
/// <inheritdoc />
public override bool Equals(object? obj) => obj is DispatcherPriority other && Equals(other);
/// <inheritdoc />
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;
/// <inheritdoc />
public int CompareTo(DispatcherPriority other) => Value.CompareTo(other.Value);
} }
} }

4
src/Avalonia.Base/Threading/DispatcherTimer.cs

@ -123,7 +123,7 @@ namespace Avalonia.Threading
/// <param name="interval">The interval at which to tick.</param> /// <param name="interval">The interval at which to tick.</param>
/// <param name="priority">The priority to use.</param> /// <param name="priority">The priority to use.</param>
/// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns> /// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
public static IDisposable Run(Func<bool> action, TimeSpan interval, DispatcherPriority priority = DispatcherPriority.Normal) public static IDisposable Run(Func<bool> action, TimeSpan interval, DispatcherPriority priority = default)
{ {
var timer = new DispatcherTimer(priority) { Interval = interval }; var timer = new DispatcherTimer(priority) { Interval = interval };
@ -152,7 +152,7 @@ namespace Avalonia.Threading
public static IDisposable RunOnce( public static IDisposable RunOnce(
Action action, Action action,
TimeSpan interval, TimeSpan interval,
DispatcherPriority priority = DispatcherPriority.Normal) DispatcherPriority priority = default)
{ {
interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1);

32
src/Avalonia.Base/Threading/IDispatcher.cs

@ -24,16 +24,7 @@ namespace Avalonia.Threading
/// </summary> /// </summary>
/// <param name="action">The method.</param> /// <param name="action">The method.</param>
/// <param name="priority">The priority with which to invoke the method.</param> /// <param name="priority">The priority with which to invoke the method.</param>
void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal); void Post(Action action, DispatcherPriority priority = default);
/// <summary>
/// Posts an action that will be invoked on the dispatcher thread.
/// </summary>
/// <typeparam name="T">type of argument</typeparam>
/// <param name="action">The method to call.</param>
/// <param name="arg">The argument of method to call.</param>
/// <param name="priority">The priority with which to invoke the method.</param>
void Post<T>(Action<T> action, T arg, DispatcherPriority priority = DispatcherPriority.Normal);
/// <summary> /// <summary>
/// Invokes a action on the dispatcher thread. /// Invokes a action on the dispatcher thread.
@ -41,24 +32,7 @@ namespace Avalonia.Threading
/// <param name="action">The method.</param> /// <param name="action">The method.</param>
/// <param name="priority">The priority with which to invoke the method.</param> /// <param name="priority">The priority with which to invoke the method.</param>
/// <returns>A task that can be used to track the method's execution.</returns> /// <returns>A task that can be used to track the method's execution.</returns>
Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal); Task InvokeAsync(Action action, DispatcherPriority priority = default);
/// <summary>
/// Invokes a method on the dispatcher thread.
/// </summary>
/// <param name="function">The method.</param>
/// <param name="priority">The priority with which to invoke the method.</param>
/// <returns>A task that can be used to track the method's execution.</returns>
Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority = DispatcherPriority.Normal);
/// <summary>
/// Queues the specified work to run on the dispatcher thread and returns a proxy for the
/// task returned by <paramref name="function"/>.
/// </summary>
/// <param name="function">The work to execute asynchronously.</param>
/// <param name="priority">The priority with which to invoke the method.</param>
/// <returns>A task that represents a proxy for the task returned by <paramref name="function"/>.</returns>
Task InvokeAsync(Func<Task> function, DispatcherPriority priority = DispatcherPriority.Normal);
/// <summary> /// <summary>
/// Queues the specified work to run on the dispatcher thread and returns a proxy for the /// Queues the specified work to run on the dispatcher thread and returns a proxy for the
@ -67,6 +41,6 @@ namespace Avalonia.Threading
/// <param name="function">The work to execute asynchronously.</param> /// <param name="function">The work to execute asynchronously.</param>
/// <param name="priority">The priority with which to invoke the method.</param> /// <param name="priority">The priority with which to invoke the method.</param>
/// <returns>A task that represents a proxy for the task returned by <paramref name="function"/>.</returns> /// <returns>A task that represents a proxy for the task returned by <paramref name="function"/>.</returns>
Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function, DispatcherPriority priority = DispatcherPriority.Normal); Task InvokeAsync(Func<Task> function, DispatcherPriority priority = default);
} }
} }

19
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<T>(ReadOnlySpan<char> key, bool ignoreCase) where T : struct
{
return Enum.Parse<T>(key, ignoreCase);
}
#else
public static T Parse<T>(string key, bool ignoreCase) where T : struct
{
return (T)Enum.Parse(typeof(T), key, ignoreCase);
}
#endif
}
}

32
src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs

@ -1,32 +0,0 @@
namespace Avalonia.Utilities
{
/// <summary>
/// A visitor to resolve an untyped <see cref="AvaloniaProperty"/> to a typed property.
/// </summary>
/// <typeparam name="TData">The type of user data passed.</typeparam>
/// <remarks>
/// Pass an instance that implements this interface to
/// <see cref="AvaloniaProperty.Accept{TData}(IAvaloniaPropertyVisitor{TData}, ref TData)"/>
/// in order to resolve un untyped <see cref="AvaloniaProperty"/> to a typed
/// <see cref="StyledPropertyBase{TValue}"/> or <see cref="DirectPropertyBase{TValue}"/>.
/// </remarks>
public interface IAvaloniaPropertyVisitor<TData>
where TData : struct
{
/// <summary>
/// Called when the property is a styled property.
/// </summary>
/// <typeparam name="T">The property value type.</typeparam>
/// <param name="property">The property.</param>
/// <param name="data">The user data.</param>
void Visit<T>(StyledPropertyBase<T> property, ref TData data);
/// <summary>
/// Called when the property is a direct property.
/// </summary>
/// <typeparam name="T">The property value type.</typeparam>
/// <param name="property">The property.</param>
/// <param name="data">The user data.</param>
void Visit<T>(DirectPropertyBase<T> property, ref TData data);
}
}

29
src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs

@ -9,4 +9,31 @@ namespace Avalonia.Utilities;
public interface IWeakEventSubscriber<in TEventArgs> where TEventArgs : EventArgs public interface IWeakEventSubscriber<in TEventArgs> where TEventArgs : EventArgs
{ {
void OnEvent(object? sender, WeakEvent ev, TEventArgs e); void OnEvent(object? sender, WeakEvent ev, TEventArgs e);
} }
public sealed class WeakEventSubscriber<TEventArgs> : IWeakEventSubscriber<TEventArgs> where TEventArgs : EventArgs
{
public event Action<object?, WeakEvent, TEventArgs>? Event;
void IWeakEventSubscriber<TEventArgs>.OnEvent(object? sender, WeakEvent ev, TEventArgs e)
{
Event?.Invoke(sender, ev, e);
}
}
public sealed class TargetWeakEventSubscriber<TTarget, TEventArgs> : IWeakEventSubscriber<TEventArgs> where TEventArgs : EventArgs
{
private readonly TTarget _target;
private readonly Action<TTarget, object?, WeakEvent, TEventArgs> _dispatchFunc;
public TargetWeakEventSubscriber(TTarget target, Action<TTarget, object?, WeakEvent, TEventArgs> dispatchFunc)
{
_target = target;
_dispatchFunc = dispatchFunc;
}
void IWeakEventSubscriber<TEventArgs>.OnEvent(object? sender, WeakEvent ev, TEventArgs e)
{
_dispatchFunc(_target, sender, ev, e);
}
}

138
src/Avalonia.Base/Utilities/WeakEvent.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -36,7 +37,7 @@ public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : Event
{ {
if (!_subscriptions.TryGetValue(target, out var subscription)) if (!_subscriptions.TryGetValue(target, out var subscription))
_subscriptions.Add(target, subscription = new Subscription(this, target)); _subscriptions.Add(target, subscription = new Subscription(this, target));
subscription.Add(new WeakReference<IWeakEventSubscriber<TEventArgs>>(subscriber)); subscription.Add(subscriber);
} }
public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber) public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
@ -51,11 +52,59 @@ public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : Event
private readonly TSender _target; private readonly TSender _target;
private readonly Action _compact; private readonly Action _compact;
private WeakReference<IWeakEventSubscriber<TEventArgs>>?[] _data = struct Entry
new WeakReference<IWeakEventSubscriber<TEventArgs>>[16]; {
private int _count; WeakReference<IWeakEventSubscriber<TEventArgs>>? _reference;
int _hashCode;
public Entry(IWeakEventSubscriber<TEventArgs> r)
{
if (r == null)
{
_reference = null;
_hashCode = 0;
return;
}
_hashCode = r.GetHashCode();
_reference = new WeakReference<IWeakEventSubscriber<TEventArgs>>(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<TEventArgs> target)
{
if (_reference == null)
{
target = null!;
return false;
}
return _reference.TryGetTarget(out target);
}
public bool Equals(IWeakEventSubscriber<TEventArgs> r)
{
if (_reference == null || r.GetHashCode() != _hashCode)
return false;
return _reference.TryGetTarget(out var target) && target == r;
}
}
private readonly Action _unsubscribe; private readonly Action _unsubscribe;
private readonly WeakHashList<IWeakEventSubscriber<TEventArgs>> _list = new();
private bool _compactScheduled; private bool _compactScheduled;
private bool _destroyed;
public Subscription(WeakEvent<TSender, TEventArgs> ev, TSender target) public Subscription(WeakEvent<TSender, TEventArgs> ev, TSender target)
{ {
@ -67,48 +116,27 @@ public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : Event
void Destroy() void Destroy()
{ {
if(_destroyed)
return;
_destroyed = true;
_unsubscribe(); _unsubscribe();
_ev._subscriptions.Remove(_target); _ev._subscriptions.Remove(_target);
} }
public void Add(WeakReference<IWeakEventSubscriber<TEventArgs>> s) public void Add(IWeakEventSubscriber<TEventArgs> s) => _list.Add(s);
{
if (_count == _data.Length)
{
//Extend capacity
var extendedData = new WeakReference<IWeakEventSubscriber<TEventArgs>>?[_data.Length * 2];
Array.Copy(_data, extendedData, _data.Length);
_data = extendedData;
}
_data[_count] = s;
_count++;
}
public void Remove(IWeakEventSubscriber<TEventArgs> s) public void Remove(IWeakEventSubscriber<TEventArgs> s)
{ {
var removed = false; _list.Remove(s);
if(_list.IsEmpty)
for (int c = 0; c < _count; ++c) Destroy();
{ else if(_list.NeedCompact && _compactScheduled)
var reference = _data[c];
if (reference != null && reference.TryGetTarget(out var instance) && instance == s)
{
_data[c] = null;
removed = true;
}
}
if (removed)
{
ScheduleCompact(); ScheduleCompact();
}
} }
void ScheduleCompact() void ScheduleCompact()
{ {
if(_compactScheduled) if(_compactScheduled || _destroyed)
return; return;
_compactScheduled = true; _compactScheduled = true;
Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background);
@ -116,43 +144,27 @@ public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : Event
void Compact() void Compact()
{ {
if(!_compactScheduled)
return;
_compactScheduled = false; _compactScheduled = false;
int empty = -1; _list.Compact();
for (var c = 0; c < _count; c++) if (_list.IsEmpty)
{
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)
Destroy(); Destroy();
} }
void OnEvent(object? sender, TEventArgs eventArgs) void OnEvent(object? sender, TEventArgs eventArgs)
{ {
var needCompact = false; var alive = _list.GetAlive();
for (var c = 0; c < _count; c++) if(alive == null)
Destroy();
else
{ {
var r = _data[c]; foreach(var item in alive.Span)
if (r?.TryGetTarget(out var sub) == true) item.OnEvent(_target, _ev, eventArgs);
sub!.OnEvent(_target, _ev, eventArgs); WeakHashList<IWeakEventSubscriber<TEventArgs>>.ReturnToSharedPool(alive);
else if(_list.NeedCompact && !_compactScheduled)
needCompact = true; ScheduleCompact();
} }
if (needCompact)
ScheduleCompact();
} }
} }

241
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<T> where T : class
{
public const int DefaultArraySize = 8;
private struct Key
{
public WeakReference<T>? 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<T>(r)
};
public override int GetHashCode() => HashCode;
}
class KeyComparer : IEqualityComparer<Key>
{
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<Key, int>? _dic;
WeakReference<T>?[]? _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<T>[DefaultArraySize];
if (_arrCount < _arr.Length)
{
_arr[_arrCount] = new WeakReference<T>(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<T>(item);
return;
}
}
_dic = new Dictionary<Key, int>(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<Key>? toRemove = null;
foreach (var kvp in _dic)
{
if (kvp.Key.Weak?.TryGetTarget(out _) != true)
(toRemove ??= new PooledList<Key>()).Add(kvp.Key);
}
if (toRemove != null)
{
foreach (var k in toRemove)
_dic.Remove(k);
toRemove.Dispose();
}
}
}
private static readonly Stack<PooledList<T>> s_listPool = new();
public static void ReturnToSharedPool(PooledList<T> list)
{
list.Clear();
s_listPool.Push(list);
}
public PooledList<T>? GetAlive(Func<PooledList<T>>? factory = null)
{
PooledList<T>? 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<T>())).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<T>()))
.Add(target!);
else
NeedCompact = true;
}
}
return pooled;
}
}

27
src/Avalonia.Base/ValueStore.cs

@ -21,16 +21,15 @@ namespace Avalonia
/// - For a single binding it will be an instance of <see cref="BindingEntry{T}"/> /// - For a single binding it will be an instance of <see cref="BindingEntry{T}"/>
/// - For all other cases it will be an instance of <see cref="PriorityValue{T}"/> /// - For all other cases it will be an instance of <see cref="PriorityValue{T}"/>
/// </remarks> /// </remarks>
internal class ValueStore : IValueSink internal class ValueStore
{ {
private readonly AvaloniaObject _owner; private readonly AvaloniaObject _owner;
private readonly IValueSink _sink;
private readonly AvaloniaPropertyValueStore<IValue> _values; private readonly AvaloniaPropertyValueStore<IValue> _values;
private BatchUpdate? _batchUpdate; private BatchUpdate? _batchUpdate;
public ValueStore(AvaloniaObject owner) public ValueStore(AvaloniaObject owner)
{ {
_sink = _owner = owner; _owner = owner;
_values = new AvaloniaPropertyValueStore<IValue>(); _values = new AvaloniaPropertyValueStore<IValue>();
} }
@ -122,7 +121,7 @@ namespace Avalonia
} }
else else
{ {
var entry = new ConstantValueEntry<T>(property, value, priority, this); var entry = new ConstantValueEntry<T>(property, value, priority, new(this));
AddValue(property, entry); AddValue(property, entry);
NotifyValueChanged<T>(property, default, value, priority); NotifyValueChanged<T>(property, default, value, priority);
result = entry; result = entry;
@ -151,7 +150,7 @@ namespace Avalonia
} }
else else
{ {
var entry = new BindingEntry<T>(_owner, property, source, priority, this); var entry = new BindingEntry<T>(_owner, property, source, priority, new(this));
AddValue(property, entry); AddValue(property, entry);
return 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 // 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 // them to a constant value entry with Unset priority in the event of a local value being
// cleared during a batch update. // cleared during a batch update.
var sentinel = new ConstantValueEntry<T>(property, Optional<T>.Empty, BindingPriority.Unset, _sink); var sentinel = new ConstantValueEntry<T>(property, Optional<T>.Empty, BindingPriority.Unset, new(this));
_values.SetValue(property, sentinel); _values.SetValue(property, sentinel);
} }
@ -196,11 +195,11 @@ namespace Avalonia
} }
} }
public void CoerceValue<T>(StyledPropertyBase<T> property) public void CoerceValue(AvaloniaProperty property)
{ {
if (TryGetValue(property, out var slot)) if (TryGetValue(property, out var slot))
{ {
if (slot is PriorityValue<T> p) if (slot is IPriorityValue p)
{ {
p.UpdateEffectiveValue(); p.UpdateEffectiveValue();
} }
@ -222,7 +221,7 @@ namespace Avalonia
return null; return null;
} }
void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) public void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{ {
if (_batchUpdate is object) if (_batchUpdate is object)
{ {
@ -233,11 +232,11 @@ namespace Avalonia
} }
else else
{ {
_sink.ValueChanged(change); _owner.ValueChanged(change);
} }
} }
void IValueSink.Completed<T>( public void Completed<T>(
StyledPropertyBase<T> property, StyledPropertyBase<T> property,
IPriorityValueEntry entry, IPriorityValueEntry entry,
Optional<T> oldValue) Optional<T> oldValue)
@ -248,7 +247,7 @@ namespace Avalonia
if (_batchUpdate is null) if (_batchUpdate is null)
{ {
_values.Remove(property); _values.Remove(property);
_sink.Completed(property, entry, oldValue); _owner.Completed(property, entry, oldValue);
} }
else else
{ {
@ -352,7 +351,7 @@ namespace Avalonia
{ {
if (_batchUpdate is null) if (_batchUpdate is null)
{ {
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>( _owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner, _owner,
property, property,
oldValue, oldValue,
@ -451,7 +450,7 @@ namespace Avalonia
}; };
// Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs<T>. // Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs<T>.
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 // 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 // the _sink.ValueChanged notification. They instead mark themselves for removal by setting

21
src/Avalonia.Base/Visual.cs

@ -97,12 +97,18 @@ namespace Avalonia
/// </summary> /// </summary>
public static readonly StyledProperty<int> ZIndexProperty = public static readonly StyledProperty<int> ZIndexProperty =
AvaloniaProperty.Register<Visual, int>(nameof(ZIndex)); AvaloniaProperty.Register<Visual, int>(nameof(ZIndex));
private static readonly WeakEvent<IAffectsRender, EventArgs> InvalidatedWeakEvent =
WeakEvent.Register<IAffectsRender>(
(s, h) => s.Invalidated += h,
(s, h) => s.Invalidated -= h);
private Rect _bounds; private Rect _bounds;
private TransformedBounds? _transformedBounds; private TransformedBounds? _transformedBounds;
private IRenderRoot? _visualRoot; private IRenderRoot? _visualRoot;
private IVisual? _visualParent; private IVisual? _visualParent;
private bool _hasMirrorTransform; private bool _hasMirrorTransform;
private TargetWeakEventSubscriber<Visual, EventArgs>? _affectsRenderWeakSubscriber;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="Visual"/> class. /// Initializes static members of the <see cref="Visual"/> class.
@ -369,12 +375,21 @@ namespace Avalonia
{ {
if (e.OldValue is IAffectsRender oldValue) if (e.OldValue is IAffectsRender oldValue)
{ {
WeakEventHandlerManager.Unsubscribe<EventArgs, T>(oldValue, nameof(oldValue.Invalidated), sender.AffectsRenderInvalidated); if (sender._affectsRenderWeakSubscriber != null)
InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber);
} }
if (e.NewValue is IAffectsRender newValue) if (e.NewValue is IAffectsRender newValue)
{ {
WeakEventHandlerManager.Subscribe<IAffectsRender, EventArgs, T>(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated); if (sender._affectsRenderWeakSubscriber == null)
{
sender._affectsRenderWeakSubscriber = new TargetWeakEventSubscriber<Visual, EventArgs>(
sender, static (target, _, _, _) =>
{
target.InvalidateVisual();
});
}
InvalidatedWeakEvent.Subscribe(newValue, sender._affectsRenderWeakSubscriber);
} }
sender.InvalidateVisual(); sender.InvalidateVisual();
@ -625,8 +640,6 @@ namespace Avalonia
OnVisualParentChanged(old, value); OnVisualParentChanged(old, value);
} }
private void AffectsRenderInvalidated(object? sender, EventArgs e) => InvalidateVisual();
/// <summary> /// <summary>
/// Called when the <see cref="VisualChildren"/> collection changes. /// Called when the <see cref="VisualChildren"/> collection changes.
/// </summary> /// </summary>

3
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -95,6 +95,9 @@
<Compile Include="../Avalonia.Controls/GridLength.cs"> <Compile Include="../Avalonia.Controls/GridLength.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link> <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile> </Compile>
<Compile Include="../Avalonia.Base/RelativePoint.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" /> <Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\**\obj\**\*.cs" /> <Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\**\obj\**\*.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" /> <Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" />

10
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"
}
}
}

5
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<T>(Avalonia.AvaloniaPropertyChangedEventArgs<T>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Controls.DataGridTextColumn.OnPropertyChanged<T>(Avalonia.AvaloniaPropertyChangedEventArgs<T>)' does not exist in the implementation but it does exist in the contract.
Total Issues: 2

18
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -32,6 +32,14 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Displays data in a customizable grid. /// Displays data in a customizable grid.
/// </summary> /// </summary>
[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")] [PseudoClasses(":invalid", ":empty-rows", ":empty-columns")]
public partial class DataGrid : TemplatedControl public partial class DataGrid : TemplatedControl
{ {
@ -669,8 +677,6 @@ namespace Avalonia.Controls
ItemsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnItemsPropertyChanged(e)); ItemsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnItemsPropertyChanged(e));
CanUserResizeColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnCanUserResizeColumnsChanged(e)); CanUserResizeColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnCanUserResizeColumnsChanged(e));
ColumnWidthProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnColumnWidthChanged(e)); ColumnWidthProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnColumnWidthChanged(e));
RowBackgroundProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowBackgroundChanged(e));
AlternatingRowBackgroundProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowBackgroundChanged(e));
FrozenColumnCountProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnFrozenColumnCountChanged(e)); FrozenColumnCountProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnFrozenColumnCountChanged(e));
GridLinesVisibilityProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnGridLinesVisibilityChanged(e)); GridLinesVisibilityProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnGridLinesVisibilityChanged(e));
HeadersVisibilityProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnHeadersVisibilityChanged(e)); HeadersVisibilityProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnHeadersVisibilityChanged(e));
@ -1144,14 +1150,6 @@ namespace Avalonia.Controls
InvalidateCellsArrange(); InvalidateCellsArrange();
} }
private void OnRowBackgroundChanged(AvaloniaPropertyChangedEventArgs e)
{
foreach (DataGridRow row in GetAllRows())
{
row.EnsureBackground();
}
}
private void OnColumnWidthChanged(AvaloniaPropertyChangedEventArgs e) private void OnColumnWidthChanged(AvaloniaPropertyChangedEventArgs e)
{ {
var value = (DataGridLength)e.NewValue; var value = (DataGridLength)e.NewValue;

1
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@ -13,6 +13,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> cell. /// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> cell.
/// </summary> /// </summary>
[TemplatePart(DATAGRIDCELL_elementRightGridLine, typeof(Rectangle))]
[PseudoClasses(":selected", ":current", ":edited", ":invalid")] [PseudoClasses(":selected", ":current", ":edited", ":invalid")]
public class DataGridCell : ContentControl public class DataGridCell : ContentControl
{ {

2
src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs

@ -46,7 +46,7 @@ namespace Avalonia.Controls
set => SetValue(IsThreeStateProperty, value); set => SetValue(IsThreeStateProperty, value);
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);

4
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -192,14 +192,14 @@ namespace Avalonia.Controls
set => SetValue(IsVisibleProperty, value); set => SetValue(IsVisibleProperty, value);
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == IsVisibleProperty) if (change.Property == IsVisibleProperty)
{ {
OwningGrid?.OnColumnVisibleStateChanging(this); OwningGrid?.OnColumnVisibleStateChanging(this);
var isVisible = (change as AvaloniaPropertyChangedEventArgs<bool>).NewValue.Value; var isVisible = change.GetNewValue<bool>();
if (_headerCell != null) if (_headerCell != null)
{ {

36
src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs

@ -12,7 +12,8 @@ namespace Avalonia.Controls
{ {
internal class DataGridColumnCollection : ObservableCollection<DataGridColumn> internal class DataGridColumnCollection : ObservableCollection<DataGridColumn>
{ {
private DataGrid _owningGrid; private readonly Dictionary<int, int> _columnsMap = new Dictionary<int, int>();
private readonly DataGrid _owningGrid;
public DataGridColumnCollection(DataGrid owningGrid) public DataGridColumnCollection(DataGrid owningGrid)
{ {
@ -124,18 +125,8 @@ namespace Avalonia.Controls
internal int VisibleColumnCount internal int VisibleColumnCount
{ {
get get;
{ private set;
int visibleColumnCount = 0;
for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++)
{
if (ItemsInternal[columnIndex].IsVisible)
{
visibleColumnCount++;
}
}
return visibleColumnCount;
}
} }
internal double VisibleEdgedColumnsWidth internal double VisibleEdgedColumnsWidth
@ -287,20 +278,31 @@ namespace Avalonia.Controls
{ {
VisibleStarColumnCount = 0; VisibleStarColumnCount = 0;
VisibleEdgedColumnsWidth = 0; VisibleEdgedColumnsWidth = 0;
VisibleColumnCount = 0;
_columnsMap.Clear();
for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++) 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(); VisibleColumnCount++;
if (ItemsInternal[columnIndex].Width.IsStar) item.EnsureWidth();
if (item.Width.IsStar)
{ {
VisibleStarColumnCount++; 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) internal DataGridColumn GetColumnAtDisplayIndex(int displayIndex)
{ {
if (displayIndex < 0 || displayIndex >= ItemsInternal.Count || displayIndex >= DisplayIndexMap.Count) if (displayIndex < 0 || displayIndex >= ItemsInternal.Count || displayIndex >= DisplayIndexMap.Count)

9
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 // We need to explicitly collapse the cells of the invisible column because layout only goes through
// visible ones // visible ones
if (!updatedColumn.IsVisible) ColumnHeaders?.InvalidateChildIndex();
foreach (var row in GetAllRows())
{ {
foreach (DataGridRow row in GetAllRows()) row.Cells[updatedColumn.Index].IsVisible = updatedColumn.IsVisible;
{ row.InvalidateCellsIndex();
row.Cells[updatedColumn.Index].IsVisible = false;
}
} }
} }

36
src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs

@ -77,7 +77,7 @@ namespace Avalonia.Controls
private set; private set;
} }
public int Count => GetCount(true); public int Count => TryGetCount(true, false, out var count) ? count : 0;
public bool DataIsPrimitive public bool DataIsPrimitive
{ {
@ -193,22 +193,25 @@ namespace Avalonia.Controls
} }
} }
internal bool Any()
{
return GetCount(false) > 0;
}
/// <param name="allowSlow">When "allowSlow" is false, method will not use Linq.Count() method and will return 0 or 1 instead.</param> /// <param name="allowSlow">When "allowSlow" is false, method will not use Linq.Count() method and will return 0 or 1 instead.</param>
private int GetCount(bool allowSlow) /// <param name="getAny">If "getAny" is true, method can use Linq.Any() method to speedup.</param>
internal bool TryGetCount(bool allowSlow, bool getAny, out int count)
{ {
return DataSource switch bool result;
(result, count) = DataSource switch
{ {
ICollection collection => collection.Count, ICollection collection => (true, collection.Count),
DataGridCollectionView cv => cv.Count, DataGridCollectionView cv => (true, cv.Count),
IEnumerable enumerable when allowSlow => enumerable.Cast<object>().Count(), IEnumerable enumerable when allowSlow && !getAny => (true, enumerable.Cast<object>().Count()),
IEnumerable enumerable when !allowSlow => enumerable.Cast<object>().Any() ? 1 : 0, IEnumerable enumerable when getAny => (true, enumerable.Cast<object>().Any() ? 1 : 0),
_ => 0 _ => (false, 0)
}; };
return result;
}
internal bool Any()
{
return TryGetCount(false, true, out var count) && count > 0;
} }
/// <summary> /// <summary>
@ -383,7 +386,7 @@ namespace Avalonia.Controls
List<string> propertyNames = TypeHelper.SplitPropertyPath(propertyName); List<string> propertyNames = TypeHelper.SplitPropertyPath(propertyName);
for (int i = 0; i < propertyNames.Count; i++) 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()) 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 // 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 // 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) if (attributes != null && attributes.Length > 0)
{ {
EditableAttribute editableAttribute = attributes[0] as EditableAttribute; var editableAttribute = (EditableAttribute)attributes[0];
Debug.Assert(editableAttribute != null);
if (!editableAttribute.AllowEdit) if (!editableAttribute.AllowEdit)
{ {
return true; return true;

46
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -21,6 +21,11 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Represents a <see cref="T:Avalonia.Controls.DataGrid" /> row. /// Represents a <see cref="T:Avalonia.Controls.DataGrid" /> row.
/// </summary> /// </summary>
[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")] [PseudoClasses(":selected", ":editing", ":invalid")]
public class DataGridRow : TemplatedControl public class DataGridRow : TemplatedControl
{ {
@ -543,7 +548,6 @@ namespace Avalonia.Controls
RootElement = e.NameScope.Find<Panel>(DATAGRIDROW_elementRoot); RootElement = e.NameScope.Find<Panel>(DATAGRIDROW_elementRoot);
if (RootElement != null) if (RootElement != null)
{ {
EnsureBackground();
UpdatePseudoClasses(); UpdatePseudoClasses();
} }
@ -668,43 +672,9 @@ namespace Avalonia.Controls
Slot = -1; Slot = -1;
} }
// Make sure the row's background is set to its correct value. It could be explicity set or inherit internal void InvalidateCellsIndex()
// DataGrid.RowBackground or DataGrid.AlternatingRowBackground
internal void EnsureBackground()
{ {
// Inherit the DataGrid's RowBackground properties only if this row doesn't explicity have a background set _cellsElement?.InvalidateChildIndex();
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;
}
}
} }
internal void EnsureFillerVisibility() internal void EnsureFillerVisibility()
@ -1092,7 +1062,7 @@ namespace Avalonia.Controls
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
if (change.Property == DataContextProperty) if (change.Property == DataContextProperty)
{ {

6
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@ -14,6 +14,12 @@ using System.Reactive.Linq;
namespace Avalonia.Controls 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")] [PseudoClasses(":pressed", ":current", ":expanded")]
public class DataGridRowGroupHeader : TemplatedControl public class DataGridRowGroupHeader : TemplatedControl
{ {

1
src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs

@ -13,6 +13,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> row header. /// Represents an individual <see cref="T:Avalonia.Controls.DataGrid" /> row header.
/// </summary> /// </summary>
[TemplatePart(DATAGRIDROWHEADER_elementRootName, typeof(Control))]
[PseudoClasses(":invalid", ":selected", ":editing", ":current")] [PseudoClasses(":invalid", ":selected", ":editing", ":current")]
public class DataGridRowHeader : ContentControl public class DataGridRowHeader : ContentControl
{ {

7
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@ -5,6 +5,7 @@
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Utilities; using Avalonia.Utilities;
using System; using System;
@ -811,7 +812,7 @@ namespace Avalonia.Controls
if (row.Slot > slotDeleted) if (row.Slot > slotDeleted)
{ {
CorrectRowAfterDeletion(row, wasRow); CorrectRowAfterDeletion(row, wasRow);
row.EnsureBackground(); _rowsPresenter?.InvalidateChildIndex(row);
} }
} }
@ -867,7 +868,7 @@ namespace Avalonia.Controls
if (row.Slot >= slotInserted) if (row.Slot >= slotInserted)
{ {
DataGrid.CorrectRowAfterInsertion(row, rowInserted); 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 the row has been recycled, reapply the BackgroundBrush
if (row.IsRecycled) if (row.IsRecycled)
{ {
row.EnsureBackground();
row.ApplyCellsState(); row.ApplyCellsState();
_rowsPresenter?.InvalidateChildIndex(row);
} }
else if (row == EditingRow) else if (row == EditingRow)
{ {

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

Loading…
Cancel
Save