Browse Source

Merge branch 'master' into osx_no_menus

pull/8061/head
Lubomir Tetak 4 years ago
committed by GitHub
parent
commit
b6b764313c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      Avalonia.sln
  2. 32
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  3. 15
      native/Avalonia.Native/src/OSX/INSWindowHolder.h
  4. 18
      native/Avalonia.Native/src/OSX/IWindowStateChanged.h
  5. 23
      native/Avalonia.Native/src/OSX/ResizeScope.h
  6. 17
      native/Avalonia.Native/src/OSX/ResizeScope.mm
  7. 1
      native/Avalonia.Native/src/OSX/SystemDialogs.mm
  8. 119
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  9. 505
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  10. 97
      native/Avalonia.Native/src/OSX/WindowImpl.h
  11. 540
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  12. 12
      native/Avalonia.Native/src/OSX/app.mm
  13. 1
      native/Avalonia.Native/src/OSX/automation.h
  14. 5
      native/Avalonia.Native/src/OSX/automation.mm
  15. 3
      native/Avalonia.Native/src/OSX/common.h
  16. 1
      native/Avalonia.Native/src/OSX/cursor.mm
  17. 6
      native/Avalonia.Native/src/OSX/main.mm
  18. 1
      native/Avalonia.Native/src/OSX/menu.h
  19. 7
      native/Avalonia.Native/src/OSX/menu.mm
  20. 4
      native/Avalonia.Native/src/OSX/rendertarget.mm
  21. 47
      native/Avalonia.Native/src/OSX/window.h
  22. 1354
      native/Avalonia.Native/src/OSX/window.mm
  23. 13
      samples/ControlCatalog/App.xaml.cs
  24. 1
      samples/ControlCatalog/ControlCatalog.csproj
  25. 3
      samples/ControlCatalog/MainView.xaml
  26. 12
      samples/ControlCatalog/MainView.xaml.cs
  27. 79
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  28. 19
      samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
  29. 2
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  30. 10
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  31. 13
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  32. 5
      samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
  33. 1
      samples/Sandbox/Sandbox.csproj
  34. 7
      src/Avalonia.Base/Animation/Animatable.cs
  35. 77
      src/Avalonia.Base/AvaloniaObject.cs
  36. 234
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  37. 23
      src/Avalonia.Base/AvaloniaProperty.cs
  38. 43
      src/Avalonia.Base/AvaloniaPropertyChangedExtensions.cs
  39. 11
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  40. 44
      src/Avalonia.Base/DirectPropertyBase.cs
  41. 2
      src/Avalonia.Base/GeometryGroup.cs
  42. 104
      src/Avalonia.Base/IAvaloniaObject.cs
  43. 8
      src/Avalonia.Base/Input/InputElement.cs
  44. 2
      src/Avalonia.Base/Input/KeyboardNavigation.cs
  45. 5
      src/Avalonia.Base/Input/Navigation/TabNavigation.cs
  46. 24
      src/Avalonia.Base/Interactivity/IInteractive.cs
  47. 59
      src/Avalonia.Base/Layout/LayoutHelper.cs
  48. 14
      src/Avalonia.Base/Layout/Layoutable.cs
  49. 4
      src/Avalonia.Base/Layout/StackLayout.cs
  50. 18
      src/Avalonia.Base/Layout/UniformGridLayout.cs
  51. 2
      src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs
  52. 5
      src/Avalonia.Base/Media/DashStyle.cs
  53. 2
      src/Avalonia.Base/Media/DrawingImage.cs
  54. 79
      src/Avalonia.Base/Media/GlyphRun.cs
  55. 28
      src/Avalonia.Base/Media/Pen.cs
  56. 4
      src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
  57. 65
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  58. 119
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  59. 179
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  60. 5
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  61. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  62. 17
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  63. 13
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  64. 2
      src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs
  65. 3
      src/Avalonia.Base/PropertyStore/IValue.cs
  66. 17
      src/Avalonia.Base/PropertyStore/IValueSink.cs
  67. 5
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  68. 67
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  69. 45
      src/Avalonia.Base/PropertyStore/ValueOwner.cs
  70. 2
      src/Avalonia.Base/Reactive/AvaloniaPropertyBindingObservable.cs
  71. 30
      src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
  72. 7
      src/Avalonia.Base/StyledElement.cs
  73. 44
      src/Avalonia.Base/StyledPropertyBase.cs
  74. 2
      src/Avalonia.Base/Styling/IStyle.cs
  75. 65
      src/Avalonia.Base/Styling/Setter.cs
  76. 2
      src/Avalonia.Base/Styling/Styles.cs
  77. 26
      src/Avalonia.Base/Threading/IDispatcher.cs
  78. 32
      src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs
  79. 29
      src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs
  80. 138
      src/Avalonia.Base/Utilities/WeakEvent.cs
  81. 241
      src/Avalonia.Base/Utilities/WeakHashList.cs
  82. 27
      src/Avalonia.Base/ValueStore.cs
  83. 21
      src/Avalonia.Base/Visual.cs
  84. 25
      src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
  85. 41
      src/Avalonia.Controls.ColorPicker/ColorChangedEventArgs.cs
  86. 28
      src/Avalonia.Controls.ColorPicker/ColorComponent.cs
  87. 18
      src/Avalonia.Controls.ColorPicker/ColorModel.cs
  88. 50
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
  89. 130
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
  90. 146
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
  91. 399
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
  92. 222
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  93. 1606
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  94. 73
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs
  95. 26
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs
  96. 116
      src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs
  97. 68
      src/Avalonia.Controls.ColorPicker/Converters/ColorToDisplayNameConverter.cs
  98. 82
      src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
  99. 51
      src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs
  100. 50
      src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs

26
Avalonia.sln

@ -169,6 +169,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{1ECC012A-8837-4AE2-9BDA-3E2857898727}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}"
@ -1963,6 +1965,30 @@ Global
{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.Build.0 = Release|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.Build.0 = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.ActiveCfg = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.Build.0 = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

32
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -7,6 +7,14 @@
objects = {
/* Begin PBXBuildFile section */
18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391E45702740FE9DD69695 /* ResizeScope.mm */; };
1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 183919BF108EB72A029F7671 /* WindowImpl.mm */; };
183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */; };
1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391BBB7782C296D424071F /* INSWindowHolder.h */; };
1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */; };
183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391CD090AA776E7E841AC9 /* WindowImpl.h */; };
18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839171D898F9BFC1373631A /* ResizeScope.h */; };
18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */ = {isa = PBXBuildFile; fileRef = 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */; };
1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; };
1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; };
1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; };
@ -35,6 +43,14 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IWindowStateChanged.h; sourceTree = "<group>"; };
183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowBaseImpl.h; sourceTree = "<group>"; };
18391676ECF0E983F4964357 /* WindowBaseImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowBaseImpl.mm; sourceTree = "<group>"; };
1839171D898F9BFC1373631A /* ResizeScope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResizeScope.h; sourceTree = "<group>"; };
183919BF108EB72A029F7671 /* WindowImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowImpl.mm; sourceTree = "<group>"; };
18391BBB7782C296D424071F /* INSWindowHolder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = INSWindowHolder.h; sourceTree = "<group>"; };
18391CD090AA776E7E841AC9 /* WindowImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowImpl.h; sourceTree = "<group>"; };
18391E45702740FE9DD69695 /* ResizeScope.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ResizeScope.mm; sourceTree = "<group>"; };
1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = "<group>"; };
1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = "<group>"; };
1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = "<group>"; };
@ -130,6 +146,14 @@
37C09D8721580FE4006A6758 /* SystemDialogs.mm */,
AB7A61F02147C815003C5833 /* Products */,
AB661C1C2148230E00291242 /* Frameworks */,
18391676ECF0E983F4964357 /* WindowBaseImpl.mm */,
183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */,
18391BBB7782C296D424071F /* INSWindowHolder.h */,
183919BF108EB72A029F7671 /* WindowImpl.mm */,
18391CD090AA776E7E841AC9 /* WindowImpl.h */,
183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */,
18391E45702740FE9DD69695 /* ResizeScope.mm */,
1839171D898F9BFC1373631A /* ResizeScope.h */,
);
sourceTree = "<group>";
};
@ -150,6 +174,11 @@
files = (
37155CE4233C00EB0034DCE9 /* menu.h in Headers */,
BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */,
183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */,
1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */,
183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */,
18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */,
18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -229,6 +258,9 @@
AB00E4F72147CA920032A60A /* main.mm in Sources */,
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */,
AB661C202148286E00291242 /* window.mm in Sources */,
1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */,
1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */,
18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

15
native/Avalonia.Native/src/OSX/INSWindowHolder.h

@ -0,0 +1,15 @@
//
// Created by Dan Walmsley on 04/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#ifndef AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H
#define AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H
struct INSWindowHolder
{
virtual AvnWindow* _Nonnull GetNSWindow () = 0;
virtual AvnView* _Nonnull GetNSView () = 0;
};
#endif //AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H

18
native/Avalonia.Native/src/OSX/IWindowStateChanged.h

@ -0,0 +1,18 @@
//
// Created by Dan Walmsley on 04/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#ifndef AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H
#define AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H
struct IWindowStateChanged
{
virtual void WindowStateChanged () = 0;
virtual void StartStateTransition () = 0;
virtual void EndStateTransition () = 0;
virtual SystemDecorations Decorations () = 0;
virtual AvnWindowState WindowState () = 0;
};
#endif //AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H

23
native/Avalonia.Native/src/OSX/ResizeScope.h

@ -0,0 +1,23 @@
//
// Created by Dan Walmsley on 04/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#ifndef AVALONIA_NATIVE_OSX_RESIZESCOPE_H
#define AVALONIA_NATIVE_OSX_RESIZESCOPE_H
#include "window.h"
#include "avalonia-native.h"
class ResizeScope
{
public:
ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason);
~ResizeScope();
private:
AvnView* _Nonnull _view;
AvnPlatformResizeReason _restore;
};
#endif //AVALONIA_NATIVE_OSX_RESIZESCOPE_H

17
native/Avalonia.Native/src/OSX/ResizeScope.mm

@ -0,0 +1,17 @@
//
// Created by Dan Walmsley on 04/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#import <AppKit/AppKit.h>
#include "ResizeScope.h"
ResizeScope::ResizeScope(AvnView *view, AvnPlatformResizeReason reason) {
_view = view;
_restore = [view getResizeReason];
[view setResizeReason:reason];
}
ResizeScope::~ResizeScope() {
[_view setResizeReason:_restore];
}

1
native/Avalonia.Native/src/OSX/SystemDialogs.mm

@ -1,5 +1,6 @@
#include "common.h"
#include "window.h"
#include "INSWindowHolder.h"
class SystemDialogs : public ComSingleObject<IAvnSystemDialogs, &IID_IAvnSystemDialogs>
{

119
native/Avalonia.Native/src/OSX/WindowBaseImpl.h

@ -0,0 +1,119 @@
//
// Created by Dan Walmsley on 04/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#ifndef AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H
#define AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H
#import "rendertarget.h"
#include "INSWindowHolder.h"
class WindowBaseImpl : public virtual ComObject,
public virtual IAvnWindowBase,
public INSWindowHolder {
private:
NSCursor *cursor;
public:
FORWARD_IUNKNOWN()
BEGIN_INTERFACE_MAP()
INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase)
END_INTERFACE_MAP()
virtual ~WindowBaseImpl() {
View = NULL;
Window = NULL;
}
AutoFitContentView *StandardContainer;
AvnView *View;
AvnWindow *Window;
ComPtr<IAvnWindowBaseEvents> BaseEvents;
ComPtr<IAvnGlContext> _glContext;
NSObject <IRenderTarget> *renderTarget;
AvnPoint lastPositionSet;
NSString *_lastTitle;
bool _shown;
bool _inResize;
WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl);
virtual HRESULT ObtainNSWindowHandle(void **ret) override;
virtual HRESULT ObtainNSWindowHandleRetained(void **ret) override;
virtual HRESULT ObtainNSViewHandle(void **ret) override;
virtual HRESULT ObtainNSViewHandleRetained(void **ret) override;
virtual AvnWindow *GetNSWindow() override;
virtual AvnView *GetNSView() override;
virtual HRESULT Show(bool activate, bool isDialog) override;
virtual bool ShouldTakeFocusOnShow();
virtual HRESULT Hide() override;
virtual HRESULT Activate() override;
virtual HRESULT SetTopMost(bool value) override;
virtual HRESULT Close() override;
virtual HRESULT GetClientSize(AvnSize *ret) override;
virtual HRESULT GetFrameSize(AvnSize *ret) override;
virtual HRESULT GetScaling(double *ret) override;
virtual HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize) override;
virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override;
virtual HRESULT Invalidate(__attribute__((unused)) AvnRect rect) override;
virtual HRESULT SetMainMenu(IAvnMenu *menu) override;
virtual HRESULT BeginMoveDrag() override;
virtual HRESULT BeginResizeDrag(__attribute__((unused)) AvnWindowEdge edge) override;
virtual HRESULT GetPosition(AvnPoint *ret) override;
virtual HRESULT SetPosition(AvnPoint point) override;
virtual HRESULT PointToClient(AvnPoint point, AvnPoint *ret) override;
virtual HRESULT PointToScreen(AvnPoint point, AvnPoint *ret) override;
virtual HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer *fb, IUnknown *dispose) override;
virtual HRESULT SetCursor(IAvnCursor *cursor) override;
virtual void UpdateCursor();
virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget **ppv) override;
virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost **retOut) override;
virtual HRESULT SetBlurEnabled(bool enable) override;
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard *clipboard, IAvnDndResultCallback *cb,
void *sourceHandle) override;
virtual bool IsDialog();
protected:
virtual NSWindowStyleMask GetStyle();
void UpdateStyle();
};
#endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H

505
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -0,0 +1,505 @@
//
// Created by Dan Walmsley on 04/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#import <AppKit/AppKit.h>
#include "common.h"
#import "window.h"
#include "menu.h"
#include "rendertarget.h"
#include "automation.h"
#import "WindowBaseImpl.h"
#import "cursor.h"
#include "ResizeScope.h"
WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) {
_shown = false;
_inResize = false;
BaseEvents = events;
_glContext = gl;
renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext:gl];
View = [[AvnView alloc] initWithParent:this];
StandardContainer = [[AutoFitContentView new] initWithContent:View];
Window = [[AvnWindow alloc] initWithParent:this];
[Window setContentView:StandardContainer];
lastPositionSet.X = 100;
lastPositionSet.Y = 100;
_lastTitle = @"";
[Window setStyleMask:NSWindowStyleMaskBorderless];
[Window setBackingType:NSBackingStoreBuffered];
[Window setOpaque:false];
}
HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) {
START_COM_CALL;
if (ret == nullptr) {
return E_POINTER;
}
*ret = (__bridge void *) View;
return S_OK;
}
HRESULT WindowBaseImpl::ObtainNSViewHandleRetained(void **ret) {
START_COM_CALL;
if (ret == nullptr) {
return E_POINTER;
}
*ret = (__bridge_retained void *) View;
return S_OK;
}
AvnWindow *WindowBaseImpl::GetNSWindow() {
return Window;
}
AvnView *WindowBaseImpl::GetNSView() {
return View;
}
HRESULT WindowBaseImpl::ObtainNSWindowHandleRetained(void **ret) {
START_COM_CALL;
if (ret == nullptr) {
return E_POINTER;
}
*ret = (__bridge_retained void *) Window;
return S_OK;
}
HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) {
START_COM_CALL;
@autoreleasepool {
SetPosition(lastPositionSet);
UpdateStyle();
[Window setTitle:_lastTitle];
if (ShouldTakeFocusOnShow() && activate) {
[Window orderFront:Window];
[Window makeKeyAndOrderFront:Window];
[Window makeFirstResponder:View];
[NSApp activateIgnoringOtherApps:YES];
} else {
[Window orderFront:Window];
}
_shown = true;
return S_OK;
}
}
bool WindowBaseImpl::ShouldTakeFocusOnShow() {
return true;
}
HRESULT WindowBaseImpl::ObtainNSWindowHandle(void **ret) {
START_COM_CALL;
if (ret == nullptr) {
return E_POINTER;
}
*ret = (__bridge void *) Window;
return S_OK;
}
HRESULT WindowBaseImpl::Hide() {
START_COM_CALL;
@autoreleasepool {
if (Window != nullptr) {
[Window orderOut:Window];
[Window restoreParentWindow];
}
return S_OK;
}
}
HRESULT WindowBaseImpl::Activate() {
START_COM_CALL;
@autoreleasepool {
if (Window != nullptr) {
[Window makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];
}
}
return S_OK;
}
HRESULT WindowBaseImpl::SetTopMost(bool value) {
START_COM_CALL;
@autoreleasepool {
[Window setLevel:value ? NSFloatingWindowLevel : NSNormalWindowLevel];
return S_OK;
}
}
HRESULT WindowBaseImpl::Close() {
START_COM_CALL;
@autoreleasepool {
if (Window != nullptr) {
auto window = Window;
Window = nullptr;
try {
// Seems to throw sometimes on application exit.
[window close];
}
catch (NSException *) {}
}
return S_OK;
}
}
HRESULT WindowBaseImpl::GetClientSize(AvnSize *ret) {
START_COM_CALL;
@autoreleasepool {
if (ret == nullptr)
return E_POINTER;
auto frame = [View frame];
ret->Width = frame.size.width;
ret->Height = frame.size.height;
return S_OK;
}
}
HRESULT WindowBaseImpl::GetFrameSize(AvnSize *ret) {
START_COM_CALL;
@autoreleasepool {
if (ret == nullptr)
return E_POINTER;
auto frame = [Window frame];
ret->Width = frame.size.width;
ret->Height = frame.size.height;
return S_OK;
}
}
HRESULT WindowBaseImpl::GetScaling(double *ret) {
START_COM_CALL;
@autoreleasepool {
if (ret == nullptr)
return E_POINTER;
if (Window == nullptr) {
*ret = 1;
return S_OK;
}
*ret = [Window backingScaleFactor];
return S_OK;
}
}
HRESULT WindowBaseImpl::SetMinMaxSize(AvnSize minSize, AvnSize maxSize) {
START_COM_CALL;
@autoreleasepool {
[Window setMinSize:ToNSSize(minSize)];
[Window setMaxSize:ToNSSize(maxSize)];
return S_OK;
}
}
HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reason) {
if (_inResize) {
return S_OK;
}
_inResize = true;
START_COM_CALL;
auto resizeBlock = ResizeScope(View, reason);
@autoreleasepool {
auto maxSize = [Window maxSize];
auto minSize = [Window minSize];
if (x < minSize.width) {
x = minSize.width;
}
if (y < minSize.height) {
y = minSize.height;
}
if (x > maxSize.width) {
x = maxSize.width;
}
if (y > maxSize.height) {
y = maxSize.height;
}
@try {
if (!_shown) {
BaseEvents->Resized(AvnSize{x, y}, reason);
}
[Window setContentSize:NSSize{x, y}];
[Window invalidateShadow];
}
@finally {
_inResize = false;
}
return S_OK;
}
}
HRESULT WindowBaseImpl::Invalidate(__attribute__((unused)) AvnRect rect) {
START_COM_CALL;
@autoreleasepool {
[View setNeedsDisplayInRect:[View frame]];
return S_OK;
}
}
HRESULT WindowBaseImpl::SetMainMenu(IAvnMenu *menu) {
START_COM_CALL;
auto nativeMenu = dynamic_cast<AvnAppMenu *>(menu);
auto nsmenu = nativeMenu->GetNative();
[Window applyMenu:nsmenu];
if ([Window isKeyWindow]) {
[Window showWindowMenuWithAppMenu];
}
return S_OK;
}
HRESULT WindowBaseImpl::BeginMoveDrag() {
START_COM_CALL;
@autoreleasepool {
auto lastEvent = [View lastMouseDownEvent];
if (lastEvent == nullptr) {
return S_OK;
}
[Window performWindowDragWithEvent:lastEvent];
return S_OK;
}
}
HRESULT WindowBaseImpl::BeginResizeDrag(__attribute__((unused)) AvnWindowEdge edge) {
START_COM_CALL;
return S_OK;
}
HRESULT WindowBaseImpl::GetPosition(AvnPoint *ret) {
START_COM_CALL;
@autoreleasepool {
if (ret == nullptr) {
return E_POINTER;
}
auto frame = [Window frame];
ret->X = frame.origin.x;
ret->Y = frame.origin.y + frame.size.height;
*ret = ConvertPointY(*ret);
return S_OK;
}
}
HRESULT WindowBaseImpl::SetPosition(AvnPoint point) {
START_COM_CALL;
@autoreleasepool {
lastPositionSet = point;
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(point))];
return S_OK;
}
}
HRESULT WindowBaseImpl::PointToClient(AvnPoint point, AvnPoint *ret) {
START_COM_CALL;
@autoreleasepool {
if (ret == nullptr) {
return E_POINTER;
}
point = ConvertPointY(point);
NSRect convertRect = [Window convertRectFromScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)];
auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y);
*ret = [View translateLocalPoint:ToAvnPoint(viewPoint)];
return S_OK;
}
}
HRESULT WindowBaseImpl::PointToScreen(AvnPoint point, AvnPoint *ret) {
START_COM_CALL;
@autoreleasepool {
if (ret == nullptr) {
return E_POINTER;
}
auto cocoaViewPoint = ToNSPoint([View translateLocalPoint:point]);
NSRect convertRect = [Window convertRectToScreen:NSMakeRect(cocoaViewPoint.x, cocoaViewPoint.y, 0.0, 0.0)];
auto cocoaScreenPoint = NSPointFromCGPoint(NSMakePoint(convertRect.origin.x, convertRect.origin.y));
*ret = ConvertPointY(ToAvnPoint(cocoaScreenPoint));
return S_OK;
}
}
HRESULT WindowBaseImpl::ThreadSafeSetSwRenderedFrame(AvnFramebuffer *fb, IUnknown *dispose) {
START_COM_CALL;
[View setSwRenderedFrame:fb dispose:dispose];
return S_OK;
}
HRESULT WindowBaseImpl::SetCursor(IAvnCursor *cursor) {
START_COM_CALL;
@autoreleasepool {
Cursor *avnCursor = dynamic_cast<Cursor *>(cursor);
this->cursor = avnCursor->GetNative();
UpdateCursor();
if (avnCursor->IsHidden()) {
[NSCursor hide];
} else {
[NSCursor unhide];
}
return S_OK;
}
}
void WindowBaseImpl::UpdateCursor() {
if (cursor != nil) {
[cursor set];
}
}
HRESULT WindowBaseImpl::CreateGlRenderTarget(IAvnGlSurfaceRenderTarget **ppv) {
START_COM_CALL;
if (View == NULL)
return E_FAIL;
*ppv = [renderTarget createSurfaceRenderTarget];
return static_cast<HRESULT>(*ppv == nil ? E_FAIL : S_OK);
}
HRESULT WindowBaseImpl::CreateNativeControlHost(IAvnNativeControlHost **retOut) {
START_COM_CALL;
if (View == NULL)
return E_FAIL;
*retOut = ::CreateNativeControlHost(View);
return S_OK;
}
HRESULT WindowBaseImpl::SetBlurEnabled(bool enable) {
START_COM_CALL;
[StandardContainer ShowBlur:enable];
return S_OK;
}
HRESULT WindowBaseImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard *clipboard, IAvnDndResultCallback *cb, void *sourceHandle) {
START_COM_CALL;
auto item = TryGetPasteboardItem(clipboard);
[item setString:@"" forType:GetAvnCustomDataType()];
if (item == nil)
return E_INVALIDARG;
if (View == NULL)
return E_FAIL;
auto nsevent = [NSApp currentEvent];
auto nseventType = [nsevent type];
// If current event isn't a mouse one (probably due to malfunctioning user app)
// attempt to forge a new one
if (!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited)
|| (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged))) {
NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)];
auto nspoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y);
CGPoint cgpoint = NSPointToCGPoint(nspoint);
auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft);
nsevent = [NSEvent eventWithCGEvent:cgevent];
CFRelease(cgevent);
}
auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:item];
auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments];
NSRect dragItemRect = {(float) point.X, (float) point.Y, [dragItemImage size].width, [dragItemImage size].height};
[dragItem setDraggingFrame:dragItemRect contents:dragItemImage];
int op = 0;
int ieffects = (int) effects;
if ((ieffects & (int) AvnDragDropEffects::Copy) != 0)
op |= NSDragOperationCopy;
if ((ieffects & (int) AvnDragDropEffects::Link) != 0)
op |= NSDragOperationLink;
if ((ieffects & (int) AvnDragDropEffects::Move) != 0)
op |= NSDragOperationMove;
[View beginDraggingSessionWithItems:@[dragItem] event:nsevent
source:CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)];
return S_OK;
}
bool WindowBaseImpl::IsDialog() {
return false;
}
NSWindowStyleMask WindowBaseImpl::GetStyle() {
return NSWindowStyleMaskBorderless;
}
void WindowBaseImpl::UpdateStyle() {
[Window setStyleMask:GetStyle()];
}

97
native/Avalonia.Native/src/OSX/WindowImpl.h

@ -0,0 +1,97 @@
//
// Created by Dan Walmsley on 04/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#ifndef AVALONIA_NATIVE_OSX_WINDOWIMPL_H
#define AVALONIA_NATIVE_OSX_WINDOWIMPL_H
#import "WindowBaseImpl.h"
#include "IWindowStateChanged.h"
class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged
{
private:
bool _canResize;
bool _fullScreenActive;
SystemDecorations _decorations;
AvnWindowState _lastWindowState;
AvnWindowState _actualWindowState;
bool _inSetWindowState;
NSRect _preZoomSize;
bool _transitioningWindowState;
bool _isClientAreaExtended;
bool _isDialog;
AvnExtendClientAreaChromeHints _extendClientHints;
FORWARD_IUNKNOWN()
BEGIN_INTERFACE_MAP()
INHERIT_INTERFACE_MAP(WindowBaseImpl)
INTERFACE_MAP_ENTRY(IAvnWindow, IID_IAvnWindow)
END_INTERFACE_MAP()
virtual ~WindowImpl()
{
}
ComPtr<IAvnWindowEvents> WindowEvents;
WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl);
void HideOrShowTrafficLights ();
virtual HRESULT Show (bool activate, bool isDialog) override;
virtual HRESULT SetEnabled (bool enable) override;
virtual HRESULT SetParent (IAvnWindow* parent) override;
void StartStateTransition () override ;
void EndStateTransition () override ;
SystemDecorations Decorations () override ;
AvnWindowState WindowState () override ;
void WindowStateChanged () override ;
bool UndecoratedIsMaximized ();
bool IsZoomed ();
void DoZoom();
virtual HRESULT SetCanResize(bool value) override;
virtual HRESULT SetDecorations(SystemDecorations value) override;
virtual HRESULT SetTitle (char* utf8title) override;
virtual HRESULT SetTitleBarColor(AvnColor color) override;
virtual HRESULT GetWindowState (AvnWindowState*ret) override;
virtual HRESULT TakeFocusFromChildren () override;
virtual HRESULT SetExtendClientArea (bool enable) override;
virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override;
virtual HRESULT GetExtendTitleBarHeight (double*ret) override;
virtual HRESULT SetExtendTitleBarHeight (double value) override;
void EnterFullScreenMode ();
void ExitFullScreenMode ();
virtual HRESULT SetWindowState (AvnWindowState state) override;
virtual bool IsDialog() override;
protected:
virtual NSWindowStyleMask GetStyle() override;
};
#endif //AVALONIA_NATIVE_OSX_WINDOWIMPL_H

540
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -0,0 +1,540 @@
//
// Created by Dan Walmsley on 04/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#import <AppKit/AppKit.h>
#import "window.h"
#include "automation.h"
#include "menu.h"
#import "WindowImpl.h"
WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBaseImpl(events, gl) {
_isClientAreaExtended = false;
_extendClientHints = AvnDefaultChrome;
_fullScreenActive = false;
_canResize = true;
_decorations = SystemDecorationsFull;
_transitioningWindowState = false;
_inSetWindowState = false;
_lastWindowState = Normal;
_actualWindowState = Normal;
WindowEvents = events;
[Window setCanBecomeKeyAndMain];
[Window disableCursorRects];
[Window setTabbingMode:NSWindowTabbingModeDisallowed];
[Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
}
void WindowImpl::HideOrShowTrafficLights() {
if (Window == nil) {
return;
}
for (id subview in Window.contentView.superview.subviews) {
if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) {
NSView *titlebarView = [subview subviews][0];
for (id button in titlebarView.subviews) {
if ([button isKindOfClass:[NSButton class]]) {
if (_isClientAreaExtended) {
auto wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
[button setHidden:!wantsChrome];
} else {
[button setHidden:(_decorations != SystemDecorationsFull)];
}
[button setWantsLayer:true];
}
}
}
}
}
HRESULT WindowImpl::Show(bool activate, bool isDialog) {
START_COM_CALL;
@autoreleasepool {
_isDialog = isDialog;
WindowBaseImpl::Show(activate, isDialog);
HideOrShowTrafficLights();
return SetWindowState(_lastWindowState);
}
}
HRESULT WindowImpl::SetEnabled(bool enable) {
START_COM_CALL;
@autoreleasepool {
[Window setEnabled:enable];
return S_OK;
}
}
HRESULT WindowImpl::SetParent(IAvnWindow *parent) {
START_COM_CALL;
@autoreleasepool {
if (parent == nullptr)
return E_POINTER;
auto cparent = dynamic_cast<WindowImpl *>(parent);
if (cparent == nullptr)
return E_INVALIDARG;
// If one tries to show a child window with a minimized parent window, then the parent window will be
// restored but macOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive
// state. Detect this and explicitly restore the parent window ourselves to avoid this situation.
if (cparent->WindowState() == Minimized)
cparent->SetWindowState(Normal);
[Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary];
[cparent->Window addChildWindow:Window ordered:NSWindowAbove];
UpdateStyle();
return S_OK;
}
}
void WindowImpl::StartStateTransition() {
_transitioningWindowState = true;
}
void WindowImpl::EndStateTransition() {
_transitioningWindowState = false;
}
SystemDecorations WindowImpl::Decorations() {
return _decorations;
}
AvnWindowState WindowImpl::WindowState() {
return _lastWindowState;
}
void WindowImpl::WindowStateChanged() {
if (_shown && !_inSetWindowState && !_transitioningWindowState) {
AvnWindowState state;
GetWindowState(&state);
if (_lastWindowState != state) {
if (_isClientAreaExtended) {
if (_lastWindowState == FullScreen) {
// we exited fs.
if (_extendClientHints & AvnOSXThickTitleBar) {
Window.toolbar = [NSToolbar new];
Window.toolbar.showsBaselineSeparator = false;
}
[Window setTitlebarAppearsTransparent:true];
[StandardContainer setFrameSize:StandardContainer.frame.size];
} else if (state == FullScreen) {
// we entered fs.
if (_extendClientHints & AvnOSXThickTitleBar) {
Window.toolbar = nullptr;
}
[Window setTitlebarAppearsTransparent:false];
[StandardContainer setFrameSize:StandardContainer.frame.size];
}
}
_lastWindowState = state;
_actualWindowState = state;
WindowEvents->WindowStateChanged(state);
}
}
}
bool WindowImpl::UndecoratedIsMaximized() {
auto windowSize = [Window frame];
auto available = [Window screen].visibleFrame;
return CGRectEqualToRect(windowSize, available);
}
bool WindowImpl::IsZoomed() {
return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized();
}
void WindowImpl::DoZoom() {
switch (_decorations) {
case SystemDecorationsNone:
case SystemDecorationsBorderOnly:
[Window setFrame:[Window screen].visibleFrame display:true];
break;
case SystemDecorationsFull:
[Window performZoom:Window];
break;
}
}
HRESULT WindowImpl::SetCanResize(bool value) {
START_COM_CALL;
@autoreleasepool {
_canResize = value;
UpdateStyle();
return S_OK;
}
}
HRESULT WindowImpl::SetDecorations(SystemDecorations value) {
START_COM_CALL;
@autoreleasepool {
auto currentWindowState = _lastWindowState;
_decorations = value;
if (_fullScreenActive) {
return S_OK;
}
UpdateStyle();
HideOrShowTrafficLights();
switch (_decorations) {
case SystemDecorationsNone:
[Window setHasShadow:NO];
[Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES];
if (currentWindowState == Maximized) {
if (!UndecoratedIsMaximized()) {
DoZoom();
}
}
break;
case SystemDecorationsBorderOnly:
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES];
if (currentWindowState == Maximized) {
if (!UndecoratedIsMaximized()) {
DoZoom();
}
}
break;
case SystemDecorationsFull:
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
if (currentWindowState == Maximized) {
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
[View setFrameSize:newFrame];
}
break;
}
return S_OK;
}
}
HRESULT WindowImpl::SetTitle(char *utf8title) {
START_COM_CALL;
@autoreleasepool {
_lastTitle = [NSString stringWithUTF8String:(const char *) utf8title];
[Window setTitle:_lastTitle];
return S_OK;
}
}
HRESULT WindowImpl::SetTitleBarColor(AvnColor color) {
START_COM_CALL;
@autoreleasepool {
float a = (float) color.Alpha / 255.0f;
float r = (float) color.Red / 255.0f;
float g = (float) color.Green / 255.0f;
float b = (float) color.Blue / 255.0f;
auto nscolor = [NSColor colorWithSRGBRed:r green:g blue:b alpha:a];
// Based on the titlebar color we have to choose either light or dark
// OSX doesnt let you set a foreground color for titlebar.
if ((r * 0.299 + g * 0.587 + b * 0.114) > 186.0f / 255.0f) {
[Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantLight]];
} else {
[Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]];
}
[Window setTitlebarAppearsTransparent:true];
[Window setBackgroundColor:nscolor];
}
return S_OK;
}
HRESULT WindowImpl::GetWindowState(AvnWindowState *ret) {
START_COM_CALL;
@autoreleasepool {
if (ret == nullptr) {
return E_POINTER;
}
if (([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) {
*ret = FullScreen;
return S_OK;
}
if ([Window isMiniaturized]) {
*ret = Minimized;
return S_OK;
}
if (IsZoomed()) {
*ret = Maximized;
return S_OK;
}
*ret = Normal;
return S_OK;
}
}
HRESULT WindowImpl::TakeFocusFromChildren() {
START_COM_CALL;
@autoreleasepool {
if (Window == nil)
return S_OK;
if ([Window isKeyWindow])
[Window makeFirstResponder:View];
return S_OK;
}
}
HRESULT WindowImpl::SetExtendClientArea(bool enable) {
START_COM_CALL;
@autoreleasepool {
_isClientAreaExtended = enable;
if (enable) {
Window.titleVisibility = NSWindowTitleHidden;
[Window setTitlebarAppearsTransparent:true];
auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
if (wantsTitleBar) {
[StandardContainer ShowTitleBar:true];
} else {
[StandardContainer ShowTitleBar:false];
}
if (_extendClientHints & AvnOSXThickTitleBar) {
Window.toolbar = [NSToolbar new];
Window.toolbar.showsBaselineSeparator = false;
} else {
Window.toolbar = nullptr;
}
} else {
Window.titleVisibility = NSWindowTitleVisible;
Window.toolbar = nullptr;
[Window setTitlebarAppearsTransparent:false];
View.layer.zPosition = 0;
}
[Window setIsExtended:enable];
HideOrShowTrafficLights();
UpdateStyle();
return S_OK;
}
}
HRESULT WindowImpl::SetExtendClientAreaHints(AvnExtendClientAreaChromeHints hints) {
START_COM_CALL;
@autoreleasepool {
_extendClientHints = hints;
SetExtendClientArea(_isClientAreaExtended);
return S_OK;
}
}
HRESULT WindowImpl::GetExtendTitleBarHeight(double *ret) {
START_COM_CALL;
@autoreleasepool {
if (ret == nullptr) {
return E_POINTER;
}
*ret = [Window getExtendedTitleBarHeight];
return S_OK;
}
}
HRESULT WindowImpl::SetExtendTitleBarHeight(double value) {
START_COM_CALL;
@autoreleasepool {
[StandardContainer SetTitleBarHeightHint:value];
return S_OK;
}
}
void WindowImpl::EnterFullScreenMode() {
_fullScreenActive = true;
[Window setTitle:_lastTitle];
[Window toggleFullScreen:nullptr];
}
void WindowImpl::ExitFullScreenMode() {
[Window toggleFullScreen:nullptr];
_fullScreenActive = false;
SetDecorations(_decorations);
}
HRESULT WindowImpl::SetWindowState(AvnWindowState state) {
START_COM_CALL;
@autoreleasepool {
if (Window == nullptr) {
return S_OK;
}
if (_actualWindowState == state) {
return S_OK;
}
_inSetWindowState = true;
auto currentState = _actualWindowState;
_lastWindowState = state;
if (currentState == Normal) {
_preZoomSize = [Window frame];
}
if (_shown) {
switch (state) {
case Maximized:
if (currentState == FullScreen) {
ExitFullScreenMode();
}
lastPositionSet.X = 0;
lastPositionSet.Y = 0;
if ([Window isMiniaturized]) {
[Window deminiaturize:Window];
}
if (!IsZoomed()) {
DoZoom();
}
break;
case Minimized:
if (currentState == FullScreen) {
ExitFullScreenMode();
} else {
[Window miniaturize:Window];
}
break;
case FullScreen:
if ([Window isMiniaturized]) {
[Window deminiaturize:Window];
}
EnterFullScreenMode();
break;
case Normal:
if ([Window isMiniaturized]) {
[Window deminiaturize:Window];
}
if (currentState == FullScreen) {
ExitFullScreenMode();
}
if (IsZoomed()) {
if (_decorations == SystemDecorationsFull) {
DoZoom();
} else {
[Window setFrame:_preZoomSize display:true];
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
[View setFrameSize:newFrame];
}
}
break;
}
_actualWindowState = _lastWindowState;
WindowEvents->WindowStateChanged(_actualWindowState);
}
_inSetWindowState = false;
return S_OK;
}
}
bool WindowImpl::IsDialog() {
return _isDialog;
}
NSWindowStyleMask WindowImpl::GetStyle() {
unsigned long s = NSWindowStyleMaskBorderless;
switch (_decorations) {
case SystemDecorationsNone:
s = s | NSWindowStyleMaskFullSizeContentView;
break;
case SystemDecorationsBorderOnly:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView;
break;
case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless;
if (_canResize) {
s = s | NSWindowStyleMaskResizable;
}
break;
}
if ([Window parentWindow] == nullptr) {
s |= NSWindowStyleMaskMiniaturizable;
}
if (_isClientAreaExtended) {
s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground;
}
return s;
}

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

@ -82,18 +82,6 @@ ComPtr<IAvnApplicationEvents> _events;
_isHandlingSendEvent = oldHandling;
}
}
// This is needed for certain embedded controls
- (BOOL) isHandlingSendEvent
{
return _isHandlingSendEvent;
}
- (void)setHandlingSendEvent:(BOOL)handlingSendEvent
{
_isHandlingSendEvent = handlingSendEvent;
}
@end
extern void InitializeAvnApp(IAvnApplicationEvents* events)

1
native/Avalonia.Native/src/OSX/automation.h

@ -1,5 +1,4 @@
#import <Cocoa/Cocoa.h>
#include "window.h"
NS_ASSUME_NONNULL_BEGIN

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

@ -1,7 +1,8 @@
#include "common.h"
#include "automation.h"
#import "automation.h"
#import "window.h"
#include "AvnString.h"
#include "window.h"
#import "INSWindowHolder.h"
@interface AvnAccessibilityElement (Events)
- (void) raiseChildrenChanged;

3
native/Avalonia.Native/src/OSX/common.h

@ -27,7 +27,7 @@ extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnMenuItem* CreateAppMenuItemSeparator();
extern IAvnApplicationCommands* CreateApplicationCommands();
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
extern void SetAppMenu(IAvnMenu *menu);
extern void SetServicesMenu (IAvnMenu* menu);
extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem ();
@ -38,7 +38,6 @@ extern NSPoint ToNSPoint (AvnPoint p);
extern NSRect ToNSRect (AvnRect r);
extern AvnPoint ToAvnPoint (NSPoint p);
extern AvnPoint ConvertPointY (AvnPoint p);
extern CGFloat PrimaryDisplayHeight();
extern NSSize ToNSSize (AvnSize s);
#ifdef DEBUG
#define NSDebugLog(...) NSLog(__VA_ARGS__)

1
native/Avalonia.Native/src/OSX/cursor.mm

@ -1,6 +1,5 @@
#include "common.h"
#include "cursor.h"
#include <map>
class CursorFactory : public ComSingleObject<IAvnCursorFactory, &IID_IAvnCursorFactory>
{

6
native/Avalonia.Native/src/OSX/main.mm

@ -343,7 +343,7 @@ public:
@autoreleasepool
{
::SetAppMenu(s_appTitle, appMenu);
::SetAppMenu(appMenu);
return S_OK;
}
}
@ -428,7 +428,3 @@ AvnPoint ConvertPointY (AvnPoint p)
return p;
}
CGFloat PrimaryDisplayHeight()
{
return NSMaxY([[[NSScreen screens] firstObject] frame]);
}

1
native/Avalonia.Native/src/OSX/menu.h

@ -31,7 +31,6 @@ private:
NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem
IAvnActionCallback* _callback;
IAvnPredicateCallback* _predicate;
bool _isSeparator;
bool _isCheckable;
public:

7
native/Avalonia.Native/src/OSX/menu.mm

@ -1,7 +1,7 @@
#include "common.h"
#include "menu.h"
#include "window.h"
#import "window.h"
#include "KeyTransform.h"
#include <CoreFoundation/CoreFoundation.h>
#include <Carbon/Carbon.h> /* For kVK_ constants, and TIS functions. */
@ -74,8 +74,7 @@
AvnAppMenuItem::AvnAppMenuItem(bool isSeparator)
{
_isCheckable = false;
_isSeparator = isSeparator;
if(isSeparator)
{
_native = [NSMenuItem separatorItem];
@ -460,7 +459,7 @@ extern IAvnMenuItem* CreateAppMenuItemSeparator()
static IAvnMenu* s_appMenu = nullptr;
static NSMenuItem* s_appMenuItem = nullptr;
extern void SetAppMenu (NSString* appName, IAvnMenu* menu)
extern void SetAppMenu(IAvnMenu *menu)
{
s_appMenu = menu;

4
native/Avalonia.Native/src/OSX/rendertarget.mm

@ -1,14 +1,10 @@
#include "common.h"
#include "rendertarget.h"
#import <IOSurface/IOSurface.h>
#import <IOSurface/IOSurfaceObjC.h>
#import <QuartzCore/QuartzCore.h>
#include <OpenGL/CGLIOSurface.h>
#include <OpenGL/OpenGL.h>
#include <OpenGL/glext.h>
#include <OpenGL/gl3.h>
#include <OpenGL/gl3ext.h>
@interface IOSurfaceHolder : NSObject
@end

47
native/Avalonia.Native/src/OSX/window.h

@ -1,6 +1,10 @@
#ifndef window_h
#define window_h
#import "avalonia-native.h"
@class AvnMenu;
class WindowBaseImpl;
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
@ -9,7 +13,7 @@ class WindowBaseImpl;
-(AvnPoint) translateLocalPoint:(AvnPoint)pt;
-(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose;
-(void) onClosed;
-(AvnPixelSize) getPixelSize;
-(AvnPlatformResizeReason) getResizeReason;
-(void) setResizeReason:(AvnPlatformResizeReason)reason;
+ (AvnPoint)toAvnPoint:(CGPoint)p;
@ -19,12 +23,11 @@ class WindowBaseImpl;
-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content;
-(void) ShowTitleBar: (bool) show;
-(void) SetTitleBarHeightHint: (double) height;
-(void) SetContent: (NSView* _Nonnull) content;
-(void) ShowBlur: (bool) show;
@end
@interface AvnWindow : NSWindow <NSWindowDelegate>
+(void) closeAll;
-(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(void) setCanBecomeKeyAndMain;
-(void) pollModalSession: (NSModalSession _Nonnull) session;
@ -33,45 +36,11 @@ class WindowBaseImpl;
-(void) setEnabled: (bool) enable;
-(void) showAppMenuOnly;
-(void) showWindowMenuWithAppMenu;
-(void) applyMenu:(NSMenu* _Nullable)menu;
-(double) getScaling;
-(void) applyMenu:(AvnMenu* _Nullable)menu;
-(double) getExtendedTitleBarHeight;
-(void) setIsExtended:(bool)value;
-(bool) isDialog;
@end
struct INSWindowHolder
{
virtual AvnWindow* _Nonnull GetNSWindow () = 0;
virtual AvnView* _Nonnull GetNSView () = 0;
};
struct IWindowStateChanged
{
virtual void WindowStateChanged () = 0;
virtual void StartStateTransition () = 0;
virtual void EndStateTransition () = 0;
virtual SystemDecorations Decorations () = 0;
virtual AvnWindowState WindowState () = 0;
};
class ResizeScope
{
public:
ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason)
{
_view = view;
_restore = [view getResizeReason];
[view setResizeReason:reason];
}
~ResizeScope()
{
[_view setResizeReason:_restore];
}
private:
AvnView* _Nonnull _view;
AvnPlatformResizeReason _restore;
};
#endif /* window_h */

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

File diff suppressed because it is too large

13
samples/ControlCatalog/App.xaml.cs

@ -18,6 +18,16 @@ namespace ControlCatalog
DataContext = new ApplicationViewModel();
}
public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml")
};
public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml")
};
public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml")
@ -69,7 +79,8 @@ namespace ControlCatalog
public override void Initialize()
{
Styles.Insert(0, Fluent);
Styles.Insert(1, DataGridFluent);
Styles.Insert(1, ColorPickerFluent);
Styles.Insert(2, DataGridFluent);
AvaloniaXamlLoader.Load(this);
}

1
samples/ControlCatalog/ControlCatalog.csproj

@ -23,6 +23,7 @@
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
<ProjectReference Include="..\SampleControls\ControlSamples.csproj" />

3
samples/ControlCatalog/MainView.xaml

@ -43,6 +43,9 @@
<TabItem Header="Clipboard">
<pages:ClipboardPage />
</TabItem>
<TabItem Header="ColorPicker">
<pages:ColorPickerPage />
</TabItem>
<TabItem Header="ComboBox">
<pages:ComboBoxPage />
</TabItem>

12
samples/ControlCatalog/MainView.xaml.cs

@ -49,7 +49,8 @@ namespace ControlCatalog
App.Fluent.Mode = FluentThemeMode.Light;
}
Application.Current.Styles[0] = App.Fluent;
Application.Current.Styles[1] = App.DataGridFluent;
Application.Current.Styles[1] = App.ColorPickerFluent;
Application.Current.Styles[2] = App.DataGridFluent;
}
else if (theme == CatalogTheme.FluentDark)
{
@ -59,19 +60,22 @@ namespace ControlCatalog
App.Fluent.Mode = FluentThemeMode.Dark;
}
Application.Current.Styles[0] = App.Fluent;
Application.Current.Styles[1] = App.DataGridFluent;
Application.Current.Styles[1] = App.ColorPickerFluent;
Application.Current.Styles[2] = App.DataGridFluent;
}
else if (theme == CatalogTheme.DefaultLight)
{
App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Light;
Application.Current.Styles[0] = App.DefaultLight;
Application.Current.Styles[1] = App.DataGridDefault;
Application.Current.Styles[1] = App.ColorPickerDefault;
Application.Current.Styles[2] = App.DataGridDefault;
}
else if (theme == CatalogTheme.DefaultDark)
{
App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Dark;
Application.Current.Styles[0] = App.DefaultDark;
Application.Current.Styles[1] = App.DataGridDefault;
Application.Current.Styles[1] = App.ColorPickerDefault;
Application.Current.Styles[2] = App.DataGridDefault;
}
}
};

79
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -0,0 +1,79 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls"
xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="ControlCatalog.Pages.ColorPickerPage">
<UserControl.Resources>
<pc:ThirdComponentConverter x:Key="ThirdComponent" />
</UserControl.Resources>
<Grid ColumnDefinitions="Auto,10,Auto">
<Grid Grid.Column="0"
Grid.Row="0"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
<ColorSpectrum x:Name="ColorSpectrum1"
Grid.Row="0"
Color="Red"
CornerRadius="10"
Height="256"
Width="256" />
<ColorSlider Grid.Row="1"
Margin="0,10,0,0"
ColorComponent="Component1"
ColorModel="Hsva"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorSlider Grid.Row="2"
ColorComponent="Component2"
ColorModel="Hsva"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorSlider Grid.Row="3"
ColorComponent="Component3"
ColorModel="Hsva"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorSlider Grid.Row="4"
ColorComponent="Alpha"
ColorModel="Hsva"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorPreviewer Grid.Row="5"
ShowAccentColors="True"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
</Grid>
<Grid Grid.Column="2"
Grid.Row="0"
ColumnDefinitions="Auto,Auto,Auto"
RowDefinitions="Auto,Auto">
<ColorSlider Grid.Column="0"
Grid.Row="0"
IsAlphaMaxForced="True"
IsSaturationValueMaxForced="False"
ColorComponent="{Binding Components, ElementName=ColorSpectrum2, Converter={StaticResource ThirdComponent}}"
ColorModel="Hsva"
Orientation="Vertical"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
<ColorSpectrum x:Name="ColorSpectrum2"
Grid.Column="1"
Grid.Row="0"
Color="Green"
Shape="Ring"
Height="256"
Width="256" />
<ColorSlider Grid.Column="2"
Grid.Row="0"
ColorComponent="Alpha"
ColorModel="Hsva"
Orientation="Vertical"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
<ColorPreviewer Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="1"
ShowAccentColors="True"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
</Grid>
</Grid>
</UserControl>

19
samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
{
public partial class ColorPickerPage : UserControl
{
public ColorPickerPage()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

2
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

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

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

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

5
samples/SampleControls/HamburgerMenu/HamburgerMenu.cs

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

1
samples/Sandbox/Sandbox.csproj

@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
</ItemGroup>

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)
{
var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>();
var newTransitions = change.NewValue.GetValueOrDefault<Transitions>();
var e = (AvaloniaPropertyChangedEventArgs<Transitions?>)change;
var oldTransitions = e.OldValue.GetValueOrDefault();
var newTransitions = e.NewValue.GetValueOrDefault();
// When transitions are replaced, we add the new transitions before removing the old
// transitions, so that when the old transition being disposed causes the value to

77
src/Avalonia.Base/AvaloniaObject.cs

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

234
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -25,7 +25,7 @@ namespace Avalonia
}
/// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>.
/// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
@ -44,7 +44,7 @@ namespace Avalonia
}
/// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>.
/// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <typeparam name="T">The property type.</typeparam>
@ -64,7 +64,7 @@ namespace Avalonia
}
/// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>.
/// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
@ -85,7 +85,7 @@ namespace Avalonia
}
/// <summary>
/// Gets an observable for a <see cref="AvaloniaProperty"/>.
/// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <typeparam name="T">The property type.</typeparam>
@ -128,7 +128,7 @@ namespace Avalonia
}
/// <summary>
/// Gets a subject for a <see cref="AvaloniaProperty"/>.
/// Gets a subject for an <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
@ -150,7 +150,7 @@ namespace Avalonia
}
/// <summary>
/// Gets a subject for a <see cref="AvaloniaProperty"/>.
/// Gets a subject for an <see cref="AvaloniaProperty"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="o">The object.</param>
@ -230,30 +230,7 @@ namespace Avalonia
}
/// <summary>
/// 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<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.
/// Binds an <see cref="AvaloniaProperty"/> to an observable.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="target">The object.</param>
@ -273,42 +250,22 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property));
source = source ?? throw new ArgumentNullException(nameof(source));
return property switch
if (target is AvaloniaObject ao)
{
StyledPropertyBase<T> styled => target.Bind(styled, source, priority),
DirectPropertyBase<T> direct => target.Bind(direct, source),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
};
}
return property switch
{
StyledPropertyBase<T> styled => ao.Bind(styled, source, priority),
DirectPropertyBase<T> direct => ao.Bind(direct, source),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
};
}
/// <summary>
/// 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));
throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
return target.Bind(
property,
source.ToBindingValue(),
priority);
}
/// <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>
@ -334,7 +291,7 @@ namespace Avalonia
}
/// <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>
/// <param name="target">The object.</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>
/// Gets a <see cref="AvaloniaProperty"/> value.
/// </summary>
@ -436,12 +343,18 @@ namespace Avalonia
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
return property switch
if (target is AvaloniaObject ao)
{
StyledPropertyBase<T> styled => target.GetValue(styled),
DirectPropertyBase<T> direct => target.GetValue(direct),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
};
return property switch
{
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>
@ -456,7 +369,7 @@ namespace Avalonia
/// <see cref="AvaloniaProperty.UnsetValue"/>. Note that this method does not return
/// 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>
public static object? GetBaseValue(
this IAvaloniaObject target,
@ -466,7 +379,9 @@ namespace Avalonia
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
return property.RouteGetBaseValue(target, maxPriority);
if (target is AvaloniaObject ao)
return property.RouteGetBaseValue(ao, maxPriority);
throw new NotSupportedException("Custom implementations of IAvaloniaObject not supported.");
}
/// <summary>
@ -481,8 +396,7 @@ namespace Avalonia
/// <see cref="Optional{T}.Empty"/>. Note that this method does not return property values
/// that come from inherited or default values.
///
/// For direct properties returns
/// <see cref="IAvaloniaObject.GetValue{T}(DirectPropertyBase{T})"/>.
/// For direct properties returns the current value of the property.
/// </remarks>
public static Optional<T> GetBaseValue<T>(
this IAvaloniaObject target,
@ -492,69 +406,18 @@ namespace Avalonia
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
return property switch
if (target is AvaloniaObject ao)
{
StyledPropertyBase<T> styled => target.GetBaseValue(styled, maxPriority),
DirectPropertyBase<T> direct => target.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));
return property switch
{
StyledPropertyBase<T> styled => ao.GetBaseValue(styled, maxPriority),
DirectPropertyBase<T> direct => ao.GetValue(direct),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
};
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>
@ -622,17 +485,6 @@ namespace Avalonia
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>
/// Observer method for <see cref="AddClassHandler{TTarget}(IObservable{AvaloniaPropertyChangedEventArgs},
/// Func{TTarget, Action{AvaloniaPropertyChangedEventArgs}})"/>.

23
src/Avalonia.Base/AvaloniaProperty.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Styling;
using Avalonia.Utilities;
namespace Avalonia
@ -454,33 +455,24 @@ namespace Avalonia
return Name;
}
/// <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>
/// Routes an untyped ClearValue call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
internal abstract void RouteClearValue(IAvaloniaObject o);
internal abstract void RouteClearValue(AvaloniaObject o);
/// <summary>
/// Routes an untyped GetValue call to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
internal abstract object? RouteGetValue(IAvaloniaObject o);
internal abstract object? RouteGetValue(AvaloniaObject o);
/// <summary>
/// Routes an untyped GetBaseValue call to a typed call.
/// </summary>
/// <param name="o">The object instance.</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>
/// 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.
/// </returns>
internal abstract IDisposable? RouteSetValue(
IAvaloniaObject o,
AvaloniaObject o,
object? value,
BindingPriority priority);
@ -503,11 +495,12 @@ namespace Avalonia
/// <param name="source">The binding source.</param>
/// <param name="priority">The priority.</param>
internal abstract IDisposable RouteBind(
IAvaloniaObject o,
AvaloniaObject o,
IObservable<BindingValue<object?>> source,
BindingPriority priority);
internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject? oldParent);
internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent);
internal abstract ISetterInstance CreateSetterInstance(IStyleable target, object? value);
/// <summary>
/// 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()!);
}
}
}

11
src/Avalonia.Base/Controls/ResourceNodeExtensions.cs

@ -40,19 +40,16 @@ namespace Avalonia.Controls
control = control ?? throw new ArgumentNullException(nameof(control));
key = key ?? throw new ArgumentNullException(nameof(key));
IResourceHost? current = control;
IResourceNode? current = control;
while (current != null)
{
if (current is IResourceHost host)
if (current.TryGetResource(key, out value))
{
if (host.TryGetResource(key, out value))
{
return true;
}
return true;
}
current = (current as IStyledElement)?.StylingParent as IResourceHost;
current = (current as IStyledElement)?.StylingParent as IResourceNode;
}
value = null;

44
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,6 +1,7 @@
using System;
using Avalonia.Data;
using Avalonia.Reactive;
using Avalonia.Styling;
using Avalonia.Utilities;
namespace Avalonia
@ -121,31 +122,25 @@ namespace Avalonia
}
/// <inheritdoc/>
public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> visitor, ref TData data)
{
visitor.Visit(this, ref data);
}
/// <inheritdoc/>
internal override void RouteClearValue(IAvaloniaObject o)
internal override void RouteClearValue(AvaloniaObject o)
{
o.ClearValue<TValue>(this);
}
/// <inheritdoc/>
internal override object? RouteGetValue(IAvaloniaObject o)
internal override object? RouteGetValue(AvaloniaObject o)
{
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);
}
/// <inheritdoc/>
internal override IDisposable? RouteSetValue(
IAvaloniaObject o,
AvaloniaObject o,
object? value,
BindingPriority priority)
{
@ -169,7 +164,7 @@ namespace Avalonia
/// <inheritdoc/>
internal override IDisposable RouteBind(
IAvaloniaObject o,
AvaloniaObject o,
IObservable<BindingValue<object?>> source,
BindingPriority priority)
{
@ -177,9 +172,34 @@ namespace Avalonia
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.");
}
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;
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

104
src/Avalonia.Base/IAvaloniaObject.cs

@ -17,42 +17,14 @@ namespace Avalonia
/// Clears an <see cref="AvaloniaProperty"/>'s local value.
/// </summary>
/// <param name="property">The property.</param>
void ClearValue<T>(StyledPropertyBase<T> 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);
void ClearValue(AvaloniaProperty 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>(DirectPropertyBase<T> 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);
object? GetValue(AvaloniaProperty property);
/// <summary>
/// Checks whether a <see cref="AvaloniaProperty"/> is animating.
@ -71,93 +43,35 @@ namespace Avalonia
/// <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>
/// <param name="priority">The priority of the value.</param>
/// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
IDisposable? SetValue<T>(
StyledPropertyBase<T> property,
T value,
IDisposable? SetValue(
AvaloniaProperty property,
object? value,
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>
/// 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>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
IDisposable Bind<T>(
StyledPropertyBase<T> property,
IObservable<BindingValue<T>> source,
IDisposable Bind(
AvaloniaProperty property,
IObservable<object?> source,
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>
/// Coerces the specified <see cref="AvaloniaProperty"/>.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
void CoerceValue<T>(StyledPropertyBase<T> 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);
void CoerceValue(AvaloniaProperty property);
}
}

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);
if (change.Property == IsFocusedProperty)
{
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>(), null);
UpdatePseudoClasses(change.GetNewValue<bool>(), null);
}
else if (change.Property == IsPointerOverProperty)
{
UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault<bool>());
UpdatePseudoClasses(null, change.GetNewValue<bool>());
}
else if (change.Property == IsKeyboardFocusWithinProperty)
{
PseudoClasses.Set(":focus-within", change.NewValue.GetValueOrDefault<bool>());
PseudoClasses.Set(":focus-within", change.GetNewValue<bool>());
}
}

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

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

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

@ -610,7 +610,7 @@ namespace Avalonia.Input.Navigation
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);
@ -655,8 +655,9 @@ namespace Avalonia.Input.Navigation
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 IsGroup(IInputElement e) => GetKeyNavigationMode(e) != KeyboardNavigationMode.Continue;

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

@ -28,21 +28,6 @@ namespace Avalonia.Interactivity
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
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>
/// Removes a handler for the specified routed event.
/// </summary>
@ -50,15 +35,6 @@ namespace Avalonia.Interactivity
/// <param name="handler">The handler.</param>
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>
/// Adds the object's handlers for a routed event to an event route.
/// </summary>

59
src/Avalonia.Base/Layout/LayoutHelper.cs

@ -52,16 +52,44 @@ namespace Avalonia.Layout
public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding, Thickness borderThickness)
{
return ArrangeChild(child, availableSize, padding + borderThickness);
if (IsParentLayoutRounded(child, out double scale))
{
padding = RoundLayoutThickness(padding, scale, scale);
borderThickness = RoundLayoutThickness(borderThickness, scale, scale);
}
return ArrangeChildInternal(child, availableSize, padding + borderThickness);
}
public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding)
{
if(IsParentLayoutRounded(child, out double scale))
padding = RoundLayoutThickness(padding, scale, scale);
return ArrangeChildInternal(child, availableSize, padding);
}
private static Size ArrangeChildInternal(ILayoutable? child, Size availableSize, Thickness padding)
{
child?.Arrange(new Rect(availableSize).Deflate(padding));
return availableSize;
}
private static bool IsParentLayoutRounded(ILayoutable? child, out double scale)
{
var layoutableParent = (ILayoutable?)child?.GetVisualParent();
if (layoutableParent == null || !((Layoutable)layoutableParent).UseLayoutRounding)
{
scale = 1.0;
return false;
}
scale = GetLayoutScale(layoutableParent);
return true;
}
/// <summary>
/// Invalidates measure for given control and all visual children recursively.
/// </summary>
@ -126,6 +154,32 @@ namespace Avalonia.Layout
return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY));
}
/// <summary>
/// Rounds a thickness to integer values for layout purposes, compensating for high DPI screen
/// coordinates.
/// </summary>
/// <param name="thickness">Input thickness.</param>
/// <param name="dpiScaleX">DPI along x-dimension.</param>
/// <param name="dpiScaleY">DPI along y-dimension.</param>
/// <returns>Value of thickness that will be rounded under screen DPI.</returns>
/// <remarks>
/// This is a layout helper method. It takes DPI into account and also does not return
/// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper
/// associated with the UseLayoutRounding property and should not be used as a general rounding
/// utility.
/// </remarks>
public static Thickness RoundLayoutThickness(Thickness thickness, double dpiScaleX, double dpiScaleY)
{
return new Thickness(
RoundLayoutValue(thickness.Left, dpiScaleX),
RoundLayoutValue(thickness.Top, dpiScaleY),
RoundLayoutValue(thickness.Right, dpiScaleX),
RoundLayoutValue(thickness.Bottom, dpiScaleY)
);
}
/// <summary>
/// Calculates the value to be used for layout rounding at high DPI.
/// </summary>
@ -163,8 +217,7 @@ namespace Avalonia.Layout
return newValue;
}
/// <summary>
/// Calculates the min and max height for a control. Ported from WPF.
/// </summary>

14
src/Avalonia.Base/Layout/Layoutable.cs

@ -643,17 +643,27 @@ namespace Avalonia.Layout
{
if (IsVisible)
{
var useLayoutRounding = UseLayoutRounding;
var scale = LayoutHelper.GetLayoutScale(this);
var margin = Margin;
var originX = finalRect.X + margin.Left;
var originY = finalRect.Y + margin.Top;
// Margin has to be treated separately because the layout rounding function is not linear
// f(a + b) != f(a) + f(b)
// If the margin isn't pre-rounded some sizes will be offset by 1 pixel in certain scales.
if (useLayoutRounding)
{
margin = LayoutHelper.RoundLayoutThickness(margin, scale, scale);
}
var availableSizeMinusMargins = new Size(
Math.Max(0, finalRect.Width - margin.Left - margin.Right),
Math.Max(0, finalRect.Height - margin.Top - margin.Bottom));
var horizontalAlignment = HorizontalAlignment;
var verticalAlignment = VerticalAlignment;
var size = availableSizeMinusMargins;
var scale = LayoutHelper.GetLayoutScale(this);
var useLayoutRounding = UseLayoutRounding;
if (horizontalAlignment != HorizontalAlignment.Stretch)
{

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

@ -320,11 +320,11 @@ namespace Avalonia.Layout
InvalidateLayout();
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
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.
//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);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
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.
//i.e. the properties are the inverse of each other.
@ -484,31 +484,31 @@ namespace Avalonia.Layout
}
else if (change.Property == MinColumnSpacingProperty)
{
_minColumnSpacing = change.NewValue.GetValueOrDefault<double>();
_minColumnSpacing = change.GetNewValue<double>();
}
else if (change.Property == MinRowSpacingProperty)
{
_minRowSpacing = change.NewValue.GetValueOrDefault<double>();
_minRowSpacing = change.GetNewValue<double>();
}
else if (change.Property == ItemsJustificationProperty)
{
_itemsJustification = change.NewValue.GetValueOrDefault<UniformGridLayoutItemsJustification>();
_itemsJustification = change.GetNewValue<UniformGridLayoutItemsJustification>();
}
else if (change.Property == ItemsStretchProperty)
{
_itemsStretch = change.NewValue.GetValueOrDefault<UniformGridLayoutItemsStretch>();
_itemsStretch = change.GetNewValue<UniformGridLayoutItemsStretch>();
}
else if (change.Property == MinItemWidthProperty)
{
_minItemWidth = change.NewValue.GetValueOrDefault<double>();
_minItemWidth = change.GetNewValue<double>();
}
else if (change.Property == MinItemHeightProperty)
{
_minItemHeight = change.NewValue.GetValueOrDefault<double>();
_minItemHeight = change.GetNewValue<double>();
}
else if (change.Property == MaximumRowsOrColumnsProperty)
{
_maximumRowsOrColumns = change.NewValue.GetValueOrDefault<int>();
_maximumRowsOrColumns = change.GetNewValue<int>();
}
InvalidateLayout();

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

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

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

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

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

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

79
src/Avalonia.Base/Media/GlyphRun.cs

@ -28,6 +28,8 @@ namespace Avalonia.Media
private IReadOnlyList<Vector>? _glyphOffsets;
private IReadOnlyList<int>? _glyphClusters;
private int _offsetToFirstCharacter;
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary>
@ -49,7 +51,7 @@ namespace Avalonia.Media
IReadOnlyList<int>? glyphClusters = null,
int biDiLevel = 0)
{
_glyphTypeface = glyphTypeface;
_glyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
@ -203,8 +205,8 @@ namespace Avalonia.Media
/// </returns>
public double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter;
var distance = 0.0;
if (IsLeftToRight)
@ -223,7 +225,7 @@ namespace Avalonia.Media
}
var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null)
{
var currentCluster = GlyphClusters[glyphIndex];
@ -249,7 +251,7 @@ namespace Avalonia.Media
{
//RightToLeft
var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null)
{
if (characterIndex > GlyphClusters[0])
@ -284,13 +286,13 @@ namespace Avalonia.Media
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
{
var characterIndex = 0;
// Before
if (distance <= 0)
{
isInside = false;
if(GlyphClusters != null)
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
@ -307,11 +309,11 @@ namespace Avalonia.Media
characterIndex = GlyphIndices.Count - 1;
if(GlyphClusters != null)
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
@ -327,7 +329,7 @@ namespace Avalonia.Media
var advance = GetGlyphAdvance(index, out var cluster);
characterIndex = cluster;
if (distance > currentX && distance <= currentX + advance)
{
break;
@ -345,7 +347,7 @@ namespace Avalonia.Media
var advance = GetGlyphAdvance(index, out var cluster);
characterIndex = cluster;
if (currentX - advance < distance)
{
break;
@ -552,20 +554,20 @@ namespace Avalonia.Media
}
nextCluster = GlyphClusters[currentIndex];
}
}
int trailingLength;
if (nextCluster == cluster)
{
trailingLength = Characters.Start + Characters.Length - cluster;
trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster;
}
else
{
trailingLength = nextCluster - cluster;
}
return new CharacterHit(cluster, trailingLength);
return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength);
}
/// <summary>
@ -577,7 +579,7 @@ namespace Avalonia.Media
private double GetGlyphAdvance(int index, out int cluster)
{
cluster = GlyphClusters != null ? GlyphClusters[index] : index;
if (GlyphAdvances != null)
{
return GlyphAdvances[index];
@ -599,11 +601,18 @@ namespace Avalonia.Media
private GlyphRunMetrics CreateGlyphRunMetrics()
{
if (GlyphClusters != null && GlyphClusters.Count > 0)
{
var firstCluster = GlyphClusters[0];
_offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
}
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
var widthIncludingTrailingWhitespace = 0d;
var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount);
for (var index = 0; index < GlyphIndices.Count; index++)
{
var advance = GetGlyphAdvance(index, out _);
@ -615,7 +624,7 @@ namespace Avalonia.Media
if (IsLeftToRight)
{
for (var index = GlyphIndices.Count - glyphCount; index <GlyphIndices.Count; index++)
for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
{
width -= GetGlyphAdvance(index, out _);
}
@ -670,34 +679,40 @@ namespace Avalonia.Media
{
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{
var cluster = GlyphClusters[i];
var currentCluster = GlyphClusters[i];
var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster;
if (codepointIndex < 0)
if (!codepoint.IsWhiteSpace)
{
trailingWhitespaceLength = _characters.Length;
glyphCount = GlyphClusters.Count;
break;
}
var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
if (!codepoint.IsWhiteSpace)
var clusterLength = 1;
while(i - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
if(currentCluster == nextCluster)
{
clusterLength++;
i--;
continue;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength++;
newLineLength += clusterLength;
}
trailingWhitespaceLength++;
glyphCount++;
trailingWhitespaceLength += clusterLength;
glyphCount++;
}
}

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

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

4
src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs

@ -1,6 +1,4 @@
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Represents a base class for text formatting.

65
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -8,6 +8,8 @@ namespace Avalonia.Media.TextFormatting
{
internal class TextFormatterImpl : TextFormatter
{
private static readonly char[] s_empty = { ' ' };
/// <inheritdoc cref="TextFormatter.FormatLine"/>
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
@ -77,14 +79,14 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = textRuns[i];
if (currentLength + currentRun.Text.Length < length)
if (currentLength + currentRun.TextSourceLength < length)
{
currentLength += currentRun.TextSourceLength;
continue;
}
var firstCount = currentRun.Text.Length >= 1 ? i + 1 : i;
var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i;
var first = new List<DrawableTextRun>(firstCount);
@ -98,13 +100,13 @@ namespace Avalonia.Media.TextFormatting
var secondCount = textRuns.Count - firstCount;
if (currentLength + currentRun.Text.Length == length)
if (currentLength + currentRun.TextSourceLength == length)
{
var second = secondCount > 0 ? new List<DrawableTextRun>(secondCount) : null;
if (second != null)
{
var offset = currentRun.Text.Length >= 1 ? 1 : 0;
var offset = currentRun.TextSourceLength >= 1 ? 1 : 0;
for (var j = 0; j < secondCount; j++)
{
@ -122,16 +124,14 @@ namespace Avalonia.Media.TextFormatting
var second = new List<DrawableTextRun>(secondCount);
if (currentRun is not ShapedTextCharacters shapedTextCharacters)
if (currentRun is ShapedTextCharacters shapedTextCharacters)
{
throw new NotSupportedException("Only shaped runs can be split in between.");
}
var split = shapedTextCharacters.Split(length - currentLength);
var split = shapedTextCharacters.Split(length - currentLength);
first.Add(split.First);
first.Add(split.First);
second.Add(split.Second!);
second.Add(split.Second!);
}
for (var j = 1; j < secondCount; j++)
{
@ -267,7 +267,6 @@ namespace Avalonia.Media.TextFormatting
IReadOnlyList<ShapeableTextCharacters> textRuns, ReadOnlySlice<char> text, TextShaperOptions options)
{
var shapedRuns = new List<ShapedTextCharacters>(textRuns.Count);
var firstRun = textRuns[0];
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
@ -471,11 +470,10 @@ namespace Avalonia.Media.TextFormatting
return false;
}
private static bool TryMeasureLength(IReadOnlyList<DrawableTextRun> textRuns, int firstTextSourceIndex, double paragraphWidth, out int measuredLength)
private static bool TryMeasureLength(IReadOnlyList<DrawableTextRun> textRuns, double paragraphWidth, out int measuredLength)
{
measuredLength = 0;
var currentWidth = 0.0;
var lastCluster = firstTextSourceIndex;
foreach (var currentRun in textRuns)
{
@ -483,12 +481,17 @@ namespace Avalonia.Media.TextFormatting
{
case ShapedTextCharacters shapedTextCharacters:
{
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
var lastCluster = firstCluster;
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
{
var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
{
measuredLength += Math.Max(0, lastCluster - firstCluster);
goto found;
}
@ -496,6 +499,8 @@ namespace Avalonia.Media.TextFormatting
currentWidth += glyphInfo.GlyphAdvance;
}
measuredLength += currentRun.TextSourceLength;
break;
}
@ -506,7 +511,7 @@ namespace Avalonia.Media.TextFormatting
goto found;
}
lastCluster += currentRun.TextSourceLength;
measuredLength += currentRun.TextSourceLength;
currentWidth += currentRun.Size.Width;
break;
@ -516,11 +521,30 @@ namespace Avalonia.Media.TextFormatting
found:
measuredLength = Math.Max(0, lastCluster - firstTextSourceIndex + 1);
return measuredLength != 0;
}
/// <summary>
/// Creates an empty text line.
/// </summary>
/// <returns>The empty text line.</returns>
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties)
{
var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
var text = new ReadOnlySlice<char>(s_empty, firstTextSourceIndex, 1);
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine();
}
/// <summary>
/// Performs text wrapping returns a list of text lines.
/// </summary>
@ -535,7 +559,12 @@ namespace Avalonia.Media.TextFormatting
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
TextLineBreak? currentLineBreak)
{
if (!TryMeasureLength(textRuns, firstTextSourceIndex, paragraphWidth, out var measuredLength))
if(textRuns.Count == 0)
{
return CreateEmptyTextLine(firstTextSourceIndex,paragraphWidth, paragraphProperties);
}
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
{
measuredLength = 1;
}

119
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -10,13 +10,12 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public class TextLayout
{
private static readonly char[] s_empty = { ' ' };
private readonly ReadOnlySlice<char> _text;
private readonly ITextSource _textSource;
private readonly TextParagraphProperties _paragraphProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textStyleOverrides;
private readonly TextTrimming _textTrimming;
private int _textSourceLength;
/// <summary>
/// Initializes a new instance of the <see cref="TextLayout" /> class.
/// </summary>
@ -50,17 +49,49 @@ namespace Avalonia.Media.TextFormatting
int maxLines = 0,
IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null)
{
_text = string.IsNullOrEmpty(text) ?
new ReadOnlySlice<char>() :
new ReadOnlySlice<char>(text.AsMemory());
_paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight);
_textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
_textTrimming = textTrimming ?? TextTrimming.None;
_textStyleOverrides = textStyleOverrides;
LineHeight = lineHeight;
MaxWidth = maxWidth;
MaxHeight = maxHeight;
MaxLines = maxLines;
TextLines = CreateTextLines();
}
/// <summary>
/// Initializes a new instance of the <see cref="TextLayout" /> class.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="paragraphProperties">The default text paragraph properties.</param>
/// <param name="textTrimming">The text trimming.</param>
/// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="maxLines">The maximum number of text lines.</param>
public TextLayout(
ITextSource textSource,
TextParagraphProperties paragraphProperties,
TextTrimming? textTrimming = null,
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN,
int maxLines = 0)
{
_textSource = textSource;
_paragraphProperties = paragraphProperties;
_textTrimming = textTrimming ?? TextTrimming.None;
LineHeight = lineHeight;
@ -147,7 +178,7 @@ namespace Avalonia.Media.TextFormatting
return new Rect();
}
if (textPosition < 0 || textPosition >= _text.Length)
if (textPosition < 0 || textPosition >= _textSourceLength)
{
var lastLine = TextLines[TextLines.Count - 1];
@ -273,7 +304,7 @@ namespace Avalonia.Media.TextFormatting
return 0;
}
if (charIndex > _text.Length)
if (charIndex > _textSourceLength)
{
return TextLines.Count - 1;
}
@ -375,32 +406,11 @@ namespace Avalonia.Media.TextFormatting
height += textLine.Height;
}
/// <summary>
/// Creates an empty text line.
/// </summary>
/// <returns>The empty text line.</returns>
private TextLine CreateEmptyTextLine(int firstTextSourceIndex)
{
var flowDirection = _paragraphProperties.FlowDirection;
var properties = _paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
var text = new ReadOnlySlice<char>(s_empty, firstTextSourceIndex, 1);
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
return new TextLineImpl(textRuns, firstTextSourceIndex, 1, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
}
private IReadOnlyList<TextLine> CreateTextLines()
{
if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{
var textLine = CreateEmptyTextLine(0);
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
Bounds = new Rect(0,0,0, textLine.Height);
@ -411,26 +421,30 @@ namespace Avalonia.Media.TextFormatting
double left = double.PositiveInfinity, width = 0.0, height = 0.0;
var currentPosition = 0;
var textSource = new FormattedTextSource(_text,
_paragraphProperties.DefaultTextRunProperties, _textStyleOverrides);
_textSourceLength = 0;
TextLine? previousLine = null;
while (currentPosition < _text.Length)
while (true)
{
var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
#if DEBUG
if (textLine.Length == 0)
if(textLine == null || textLine.Length == 0)
{
throw new InvalidOperationException($"{nameof(textLine)} should not be empty.");
if(previousLine != null && previousLine.NewLineLength > 0)
{
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties);
textLines.Add(emptyTextLine);
UpdateBounds(emptyTextLine, ref left, ref width, ref height);
}
break;
}
#endif
currentPosition += textLine.Length;
_textSourceLength += textLine.Length;
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
@ -464,17 +478,16 @@ namespace Avalonia.Media.TextFormatting
{
break;
}
if (currentPosition != _text.Length || textLine.NewLineLength <= 0)
{
continue;
}
}
var emptyTextLine = CreateEmptyTextLine(currentPosition);
//Make sure the TextLayout always contains at least on empty line
if(textLines.Count == 0)
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
textLines.Add(emptyTextLine);
textLines.Add(textLine);
UpdateBounds(emptyTextLine,ref left, ref width, ref height);
UpdateBounds(textLine, ref left, ref width, ref height);
}
Bounds = new Rect(left, 0, width, height);

179
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -183,6 +183,7 @@ namespace Avalonia.Media.TextFormatting
case ShapedTextCharacters shapedRun:
{
characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
break;
}
default:
@ -251,7 +252,7 @@ namespace Avalonia.Media.TextFormatting
//Look at the left and right edge of the current run
if (currentRun.IsLeftToRight)
{
if (lastRun == null || lastRun.IsLeftToRight)
if (_flowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight))
{
if (characterIndex <= textRun.Text.Start)
{
@ -403,7 +404,7 @@ namespace Avalonia.Media.TextFormatting
var result = new List<TextBounds>(TextRuns.Count);
var lastDirection = _flowDirection;
var currentDirection = lastDirection;
var currentPosition = 0;
var currentPosition = FirstTextSourceIndex;
var currentRect = Rect.Empty;
var startX = Start;
@ -426,31 +427,42 @@ namespace Avalonia.Media.TextFormatting
if (nextRun != null)
{
if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
switch (nextRun)
{
goto skip;
}
case ShapedTextCharacters when currentRun is ShapedTextCharacters:
{
if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
{
goto skip;
}
if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
{
goto skip;
}
if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
{
goto skip;
}
if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
{
goto skip;
}
if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
{
goto skip;
}
if (currentRun.Text.End < firstTextSourceCharacterIndex)
{
goto skip;
}
if (currentRun.Text.End < firstTextSourceCharacterIndex)
{
goto skip;
}
goto noop;
goto noop;
}
default:
{
goto noop;
}
}
skip:
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
}
continue;
@ -460,7 +472,6 @@ namespace Avalonia.Media.TextFormatting
}
}
var endX = startX;
var endOffset = 0d;
@ -520,11 +531,18 @@ namespace Avalonia.Media.TextFormatting
}
default:
{
if (firstTextSourceCharacterIndex + textLength >= currentRun.Text.Start + currentRun.Text.Length)
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex + textLength)
{
endX += currentRun.Size.Width;
}
if (currentPosition < firstTextSourceCharacterIndex)
{
startX += currentRun.Size.Width;
}
currentPosition += currentRun.TextSourceLength;
break;
}
}
@ -536,36 +554,31 @@ namespace Avalonia.Media.TextFormatting
var width = endX - startX;
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
if (!MathUtilities.IsZero(width))
{
var textBounds = new TextBounds(currentRect.WithWidth(currentRect.Width + width), currentDirection);
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
currentRect = currentRect.WithWidth(currentRect.Width + width);
result[result.Count - 1] = textBounds;
}
else
{
currentRect = new Rect(startX, 0, width, Height);
var textBounds = new TextBounds(currentRect, currentDirection);
result.Add(new TextBounds(currentRect, currentDirection));
result[result.Count - 1] = textBounds;
}
else
{
currentRect = new Rect(startX, 0, width, Height);
result.Add(new TextBounds(currentRect, currentDirection));
}
}
if (currentDirection == FlowDirection.LeftToRight)
{
if (nextRun != null)
{
if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
{
break;
}
currentPosition = nextRun.Text.End;
}
else
if (currentPosition > firstTextSourceCharacterIndex + textLength)
{
if (currentPosition >= firstTextSourceCharacterIndex + textLength)
{
break;
}
break;
}
}
else
@ -575,10 +588,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
if (currentPosition != currentRun.Text.Start)
{
endX += currentRun.Size.Width - endOffset;
}
endX += currentRun.Size.Width - endOffset;
}
lastDirection = currentDirection;
@ -590,10 +600,10 @@ namespace Avalonia.Media.TextFormatting
public TextLineImpl FinalizeLine()
{
BidiReorder();
_textLineMetrics = CreateLineMetrics();
BidiReorder();
return this;
}
@ -1018,31 +1028,21 @@ namespace Avalonia.Media.TextFormatting
private TextLineMetrics CreateLineMetrics()
{
var start = 0d;
var height = 0d;
var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface;
var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
var width = 0d;
var widthIncludingWhitespace = 0d;
var trailingWhitespaceLength = 0;
var newLineLength = 0;
var ascent = 0d;
var descent = 0d;
var lineGap = 0d;
var fontRenderingEmSize = 0d;
var ascent = glyphTypeface.Ascent * scale;
var descent = glyphTypeface.Descent * scale;
var lineGap = glyphTypeface.LineGap * scale;
var lineHeight = _paragraphProperties.LineHeight;
var height = descent - ascent + lineGap;
if (_textRuns.Count == 0)
{
var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface;
fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
ascent = glyphTypeface.Ascent * scale;
height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
descent - ascent + lineGap :
lineHeight;
return new TextLineMetrics(false, height, 0, start, -ascent, 0, 0, 0);
}
var lineHeight = _paragraphProperties.LineHeight;
for (var index = 0; index < _textRuns.Count; index++)
{
@ -1078,41 +1078,11 @@ namespace Avalonia.Media.TextFormatting
}
}
switch (_paragraphProperties.FlowDirection)
if (index == _textRuns.Count - 1)
{
case FlowDirection.LeftToRight:
{
if (index == _textRuns.Count - 1)
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
}
break;
}
case FlowDirection.RightToLeft:
{
if (index == _textRuns.Count - 1)
{
var firstRun = _textRuns[0];
if (firstRun is ShapedTextCharacters shapedTextCharacters)
{
var offset = shapedTextCharacters.GlyphRun.Metrics.WidthIncludingTrailingWhitespace -
shapedTextCharacters.GlyphRun.Metrics.Width;
width = widthIncludingWhitespace +
textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset;
trailingWhitespaceLength = shapedTextCharacters.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = shapedTextCharacters.GlyphRun.Metrics.NewlineLength;
}
}
break;
}
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
}
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
@ -1166,12 +1136,15 @@ namespace Avalonia.Media.TextFormatting
}
}
start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
_paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection);
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
{
height = lineHeight;
if (lineHeight > height)
{
height = lineHeight;
}
}
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,

5
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

@ -238,6 +238,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
_levelRuns.Clear();
_resolvedLevelsBuffer.Clear();
if (types.IsEmpty)
{
return;
}
// Setup original types and working types
_originalClasses = types;
_workingClasses = _workingClassesBuffer.Add(types);

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

@ -19,6 +19,7 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.ColorPicker, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]

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

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

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

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

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

@ -5,7 +5,6 @@
/// </summary>
internal interface IPriorityValueEntry : IValue
{
void Reparent(IValueSink sink);
}
/// <summary>
@ -14,5 +13,6 @@
/// <typeparam name="T">The property type.</typeparam>
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();
void Start();
void RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
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 RaiseValueChanged(
IValueSink sink,
IAvaloniaObject owner,
AvaloniaObject owner,
AvaloniaProperty property,
Optional<object?> oldValue,
Optional<object?> newValue)
{
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.Cast<T>(),

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

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

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

@ -49,23 +49,31 @@ namespace Avalonia.Reactive
{
if (e.Property == _property)
{
T newValue;
if (e is AvaloniaPropertyChangedEventArgs<T> typed)
if (e.Sender is AvaloniaObject ao)
{
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
{
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!);
}
}
}
}

7
src/Avalonia.Base/StyledElement.cs

@ -467,7 +467,12 @@ namespace Avalonia
/// <param name="parent">The parent.</param>
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)

44
src/Avalonia.Base/StyledPropertyBase.cs

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

2
src/Avalonia.Base/Styling/IStyle.cs

@ -8,7 +8,7 @@ namespace Avalonia.Styling
/// <summary>
/// Defines the interface for styles.
/// </summary>
public interface IStyle
public interface IStyle : IResourceNode
{
/// <summary>
/// Gets a collection of child styles.

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
/// <see cref="AvaloniaObject"/> depending on a condition.
/// </remarks>
public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor<Setter.SetterVisitorData>
public class Setter : ISetter, IAnimationSetter
{
private object? _value;
@ -68,68 +68,7 @@ namespace Avalonia.Styling
throw new InvalidOperationException("Setter.Property must be set.");
}
var data = new SetterVisitorData
{
target = target,
value = Value,
};
Property.Accept(this, ref data);
return data.result!;
}
void IAvaloniaPropertyVisitor<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!);
}
return Property.CreateSetterInstance(target, Value);
}
private struct SetterVisitorData

2
src/Avalonia.Base/Styling/Styles.cs

@ -160,7 +160,7 @@ namespace Avalonia.Styling
for (var i = Count - 1; i >= 0; --i)
{
if (this[i] is IResourceProvider p && p.TryGetResource(key, out value))
if (this[i].TryGetResource(key, out value))
{
return true;
}

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

@ -26,15 +26,6 @@ namespace Avalonia.Threading
/// <param name="priority">The priority with which to invoke the method.</param>
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 = default);
/// <summary>
/// Invokes a action on the dispatcher thread.
/// </summary>
@ -43,14 +34,6 @@ namespace Avalonia.Threading
/// <returns>A task that can be used to track the method's execution.</returns>
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 = default);
/// <summary>
/// Queues the specified work to run on the dispatcher thread and returns a proxy for the
/// task returned by <paramref name="function"/>.
@ -59,14 +42,5 @@ namespace Avalonia.Threading
/// <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 = default);
/// <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<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function, DispatcherPriority priority = default);
}
}

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
{
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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
@ -36,7 +37,7 @@ public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : Event
{
if (!_subscriptions.TryGetValue(target, out var subscription))
_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)
@ -51,11 +52,59 @@ public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : Event
private readonly TSender _target;
private readonly Action _compact;
private WeakReference<IWeakEventSubscriber<TEventArgs>>?[] _data =
new WeakReference<IWeakEventSubscriber<TEventArgs>>[16];
private int _count;
struct Entry
{
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 WeakHashList<IWeakEventSubscriber<TEventArgs>> _list = new();
private bool _compactScheduled;
private bool _destroyed;
public Subscription(WeakEvent<TSender, TEventArgs> ev, TSender target)
{
@ -67,48 +116,27 @@ public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : Event
void Destroy()
{
if(_destroyed)
return;
_destroyed = true;
_unsubscribe();
_ev._subscriptions.Remove(_target);
}
public void Add(WeakReference<IWeakEventSubscriber<TEventArgs>> 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 Add(IWeakEventSubscriber<TEventArgs> s) => _list.Add(s);
public void Remove(IWeakEventSubscriber<TEventArgs> s)
{
var removed = false;
for (int c = 0; c < _count; ++c)
{
var reference = _data[c];
if (reference != null && reference.TryGetTarget(out var instance) && instance == s)
{
_data[c] = null;
removed = true;
}
}
if (removed)
{
_list.Remove(s);
if(_list.IsEmpty)
Destroy();
else if(_list.NeedCompact && _compactScheduled)
ScheduleCompact();
}
}
void ScheduleCompact()
{
if(_compactScheduled)
if(_compactScheduled || _destroyed)
return;
_compactScheduled = true;
Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background);
@ -116,43 +144,27 @@ public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : Event
void Compact()
{
if(!_compactScheduled)
return;
_compactScheduled = false;
int empty = -1;
for (var c = 0; c < _count; c++)
{
var r = _data[c];
//Mark current index as first empty
if (r == null && empty == -1)
empty = c;
//If current element isn't null and we have an empty one
if (r != null && empty != -1)
{
_data[c] = null;
_data[empty] = r;
empty++;
}
}
if (empty != -1)
_count = empty;
if (_count == 0)
_list.Compact();
if (_list.IsEmpty)
Destroy();
}
void OnEvent(object? sender, TEventArgs eventArgs)
{
var needCompact = false;
for (var c = 0; c < _count; c++)
var alive = _list.GetAlive();
if(alive == null)
Destroy();
else
{
var r = _data[c];
if (r?.TryGetTarget(out var sub) == true)
sub!.OnEvent(_target, _ev, eventArgs);
else
needCompact = true;
foreach(var item in alive.Span)
item.OnEvent(_target, _ev, eventArgs);
WeakHashList<IWeakEventSubscriber<TEventArgs>>.ReturnToSharedPool(alive);
if(_list.NeedCompact && !_compactScheduled)
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 < _arrCount; 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 all other cases it will be an instance of <see cref="PriorityValue{T}"/>
/// </remarks>
internal class ValueStore : IValueSink
internal class ValueStore
{
private readonly AvaloniaObject _owner;
private readonly IValueSink _sink;
private readonly AvaloniaPropertyValueStore<IValue> _values;
private BatchUpdate? _batchUpdate;
public ValueStore(AvaloniaObject owner)
{
_sink = _owner = owner;
_owner = owner;
_values = new AvaloniaPropertyValueStore<IValue>();
}
@ -122,7 +121,7 @@ namespace Avalonia
}
else
{
var entry = new ConstantValueEntry<T>(property, value, priority, this);
var entry = new ConstantValueEntry<T>(property, value, priority, new(this));
AddValue(property, entry);
NotifyValueChanged<T>(property, default, value, priority);
result = entry;
@ -151,7 +150,7 @@ namespace Avalonia
}
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);
return entry;
}
@ -187,7 +186,7 @@ namespace Avalonia
// so there's no way to mark them for removal at the end of a batch update. Instead convert
// them to a constant value entry with Unset priority in the event of a local value being
// cleared during a batch update.
var sentinel = new ConstantValueEntry<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);
}
@ -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 (slot is PriorityValue<T> p)
if (slot is IPriorityValue p)
{
p.UpdateEffectiveValue();
}
@ -222,7 +221,7 @@ namespace Avalonia
return null;
}
void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
public void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (_batchUpdate is object)
{
@ -233,11 +232,11 @@ namespace Avalonia
}
else
{
_sink.ValueChanged(change);
_owner.ValueChanged(change);
}
}
void IValueSink.Completed<T>(
public void Completed<T>(
StyledPropertyBase<T> property,
IPriorityValueEntry entry,
Optional<T> oldValue)
@ -248,7 +247,7 @@ namespace Avalonia
if (_batchUpdate is null)
{
_values.Remove(property);
_sink.Completed(property, entry, oldValue);
_owner.Completed(property, entry, oldValue);
}
else
{
@ -352,7 +351,7 @@ namespace Avalonia
{
if (_batchUpdate is null)
{
_sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
_owner,
property,
oldValue,
@ -451,7 +450,7 @@ namespace Avalonia
};
// 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
// 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>
public static readonly StyledProperty<int> ZIndexProperty =
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 TransformedBounds? _transformedBounds;
private IRenderRoot? _visualRoot;
private IVisual? _visualParent;
private bool _hasMirrorTransform;
private TargetWeakEventSubscriber<Visual, EventArgs>? _affectsRenderWeakSubscriber;
/// <summary>
/// Initializes static members of the <see cref="Visual"/> class.
@ -369,12 +375,21 @@ namespace Avalonia
{
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)
{
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();
@ -625,8 +640,6 @@ namespace Avalonia
OnVisualParentChanged(old, value);
}
private void AffectsRenderInvalidated(object? sender, EventArgs e) => InvalidateVisual();
/// <summary>
/// Called when the <see cref="VisualChildren"/> collection changes.
/// </summary>

25
src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<PackageId>Avalonia.Controls.ColorPicker</PackageId>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
<!-- Compatibility with old apps -->
<EmbeddedResource Include="Themes\**\*.xaml" />
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\JetBrains.Annotations.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<!--<Import Project="..\..\build\ApiDiff.props" />-->
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" />
</Project>

41
src/Avalonia.Controls.ColorPicker/ColorChangedEventArgs.cs

@ -0,0 +1,41 @@
// Portions of this source file are adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using System;
using Avalonia.Media;
namespace Avalonia.Controls
{
/// <summary>
/// Holds the details of a ColorChanged event.
/// </summary>
/// <remarks>
/// HSV color information is intentionally not provided.
/// Use <see cref="Color.ToHsv()"/> to obtain it.
/// </remarks>
public class ColorChangedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ColorChangedEventArgs"/> class.
/// </summary>
/// <param name="oldColor">The old/original color from before the change event.</param>
/// <param name="newColor">The new/updated color that triggered the change event.</param>
public ColorChangedEventArgs(Color oldColor, Color newColor)
{
OldColor = oldColor;
NewColor = newColor;
}
/// <summary>
/// Gets the old/original color from before the change event.
/// </summary>
public Color OldColor { get; private set; }
/// <summary>
/// Gets the new/updated color that triggered the change event.
/// </summary>
public Color NewColor { get; private set; }
}
}

28
src/Avalonia.Controls.ColorPicker/ColorComponent.cs

@ -0,0 +1,28 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines a specific component within a color model.
/// </summary>
public enum ColorComponent
{
/// <summary>
/// Represents the alpha component.
/// </summary>
Alpha = 0,
/// <summary>
/// Represents the first color component which is Red when RGB or Hue when HSV.
/// </summary>
Component1 = 1,
/// <summary>
/// Represents the second color component which is Green when RGB or Saturation when HSV.
/// </summary>
Component2 = 2,
/// <summary>
/// Represents the third color component which is Blue when RGB or Value when HSV.
/// </summary>
Component3 = 3
}
}

18
src/Avalonia.Controls.ColorPicker/ColorModel.cs

@ -0,0 +1,18 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines the model used to represent colors.
/// </summary>
public enum ColorModel
{
/// <summary>
/// Color is represented by hue, saturation, value and alpha components.
/// </summary>
Hsva,
/// <summary>
/// Color is represented by red, green, blue and alpha components.
/// </summary>
Rgba
}
}

50
src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs

@ -0,0 +1,50 @@
using Avalonia.Data;
using Avalonia.Media;
namespace Avalonia.Controls.Primitives
{
/// <inheritdoc/>
public partial class ColorPreviewer
{
/// <summary>
/// Defines the <see cref="HsvColor"/> property.
/// </summary>
public static readonly StyledProperty<HsvColor> HsvColorProperty =
AvaloniaProperty.Register<ColorPreviewer, HsvColor>(
nameof(HsvColor),
Colors.Transparent.ToHsv(),
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="ShowAccentColors"/> property.
/// </summary>
public static readonly StyledProperty<bool> ShowAccentColorsProperty =
AvaloniaProperty.Register<ColorPreviewer, bool>(
nameof(ShowAccentColors),
true);
/// <summary>
/// Gets or sets the currently previewed color in the HSV color model.
/// </summary>
/// <remarks>
/// Only an HSV color is supported in this control to ensure there is never any
/// loss of precision or color information. Accent colors, like the color spectrum,
/// only operate with the HSV color model.
/// </remarks>
public HsvColor HsvColor
{
get => GetValue(HsvColorProperty);
set => SetValue(HsvColorProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether accent colors are shown along
/// with the preview color.
/// </summary>
public bool ShowAccentColors
{
get => GetValue(ShowAccentColorsProperty);
set => SetValue(ShowAccentColorsProperty, value);
}
}
}

130
src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs

@ -0,0 +1,130 @@
using System;
using System.Globalization;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives.Converters;
using Avalonia.Input;
using Avalonia.Media;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Presents a preview color with optional accent colors.
/// </summary>
[TemplatePart(Name = nameof(AccentDec1Border), Type = typeof(Border))]
[TemplatePart(Name = nameof(AccentDec2Border), Type = typeof(Border))]
[TemplatePart(Name = nameof(AccentInc1Border), Type = typeof(Border))]
[TemplatePart(Name = nameof(AccentInc2Border), Type = typeof(Border))]
public partial class ColorPreviewer : TemplatedControl
{
/// <summary>
/// Event for when the selected color changes within the previewer.
/// This occurs when an accent color is pressed.
/// </summary>
public event EventHandler<ColorChangedEventArgs>? ColorChanged;
private bool eventsConnected = false;
private Border? AccentDec1Border;
private Border? AccentDec2Border;
private Border? AccentInc1Border;
private Border? AccentInc2Border;
/// <summary>
/// Initializes a new instance of the <see cref="ColorPreviewer"/> class.
/// </summary>
public ColorPreviewer() : base()
{
}
/// <summary>
/// Connects or disconnects all control event handlers.
/// </summary>
/// <param name="connected">True to connect event handlers, otherwise false.</param>
private void ConnectEvents(bool connected)
{
if (connected == true && eventsConnected == false)
{
// Add all events
if (AccentDec1Border != null) { AccentDec1Border.PointerPressed += AccentBorder_PointerPressed; }
if (AccentDec2Border != null) { AccentDec2Border.PointerPressed += AccentBorder_PointerPressed; }
if (AccentInc1Border != null) { AccentInc1Border.PointerPressed += AccentBorder_PointerPressed; }
if (AccentInc2Border != null) { AccentInc2Border.PointerPressed += AccentBorder_PointerPressed; }
eventsConnected = true;
}
else if (connected == false && eventsConnected == true)
{
// Remove all events
if (AccentDec1Border != null) { AccentDec1Border.PointerPressed -= AccentBorder_PointerPressed; }
if (AccentDec2Border != null) { AccentDec2Border.PointerPressed -= AccentBorder_PointerPressed; }
if (AccentInc1Border != null) { AccentInc1Border.PointerPressed -= AccentBorder_PointerPressed; }
if (AccentInc2Border != null) { AccentInc2Border.PointerPressed -= AccentBorder_PointerPressed; }
eventsConnected = false;
}
}
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
// Remove any existing events present if the control was previously loaded then unloaded
ConnectEvents(false);
AccentDec1Border = e.NameScope.Find<Border>(nameof(AccentDec1Border));
AccentDec2Border = e.NameScope.Find<Border>(nameof(AccentDec2Border));
AccentInc1Border = e.NameScope.Find<Border>(nameof(AccentInc1Border));
AccentInc2Border = e.NameScope.Find<Border>(nameof(AccentInc2Border));
// Must connect after controls are found
ConnectEvents(true);
base.OnApplyTemplate(e);
}
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == HsvColorProperty)
{
OnColorChanged(new ColorChangedEventArgs(
change.GetOldValue<HsvColor>().ToRgb(),
change.GetNewValue<HsvColor>().ToRgb()));
}
base.OnPropertyChanged(change);
}
/// <summary>
/// Called before the <see cref="ColorChanged"/> event occurs.
/// </summary>
/// <param name="e">The <see cref="ColorChangedEventArgs"/> defining old/new colors.</param>
protected virtual void OnColorChanged(ColorChangedEventArgs e)
{
ColorChanged?.Invoke(this, e);
}
/// <summary>
/// Event handler for when an accent color border is pressed.
/// This will update the color to the background of the pressed panel.
/// </summary>
private void AccentBorder_PointerPressed(object? sender, PointerPressedEventArgs e)
{
Border? border = sender as Border;
int accentStep = 0;
HsvColor hsvColor = HsvColor;
// Get the value component delta
try
{
accentStep = int.Parse(border?.Tag?.ToString() ?? "", CultureInfo.InvariantCulture);
}
catch { }
HsvColor newHsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep);
HsvColor oldHsvColor = HsvColor;
HsvColor = newHsvColor;
OnColorChanged(new ColorChangedEventArgs(oldHsvColor.ToRgb(), newHsvColor.ToRgb()));
}
}
}

146
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs

@ -0,0 +1,146 @@
using Avalonia.Data;
using Avalonia.Media;
namespace Avalonia.Controls.Primitives
{
/// <inheritdoc/>
public partial class ColorSlider
{
/// <summary>
/// Defines the <see cref="Color"/> property.
/// </summary>
public static readonly StyledProperty<Color> ColorProperty =
AvaloniaProperty.Register<ColorSlider, Color>(
nameof(Color),
Colors.White,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="ColorComponent"/> property.
/// </summary>
public static readonly StyledProperty<ColorComponent> ColorComponentProperty =
AvaloniaProperty.Register<ColorSlider, ColorComponent>(
nameof(ColorComponent),
ColorComponent.Component1);
/// <summary>
/// Defines the <see cref="ColorModel"/> property.
/// </summary>
public static readonly StyledProperty<ColorModel> ColorModelProperty =
AvaloniaProperty.Register<ColorSlider, ColorModel>(
nameof(ColorModel),
ColorModel.Rgba);
/// <summary>
/// Defines the <see cref="HsvColor"/> property.
/// </summary>
public static readonly StyledProperty<HsvColor> HsvColorProperty =
AvaloniaProperty.Register<ColorSlider, HsvColor>(
nameof(HsvColor),
Colors.White.ToHsv(),
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="IsAlphaMaxForced"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsAlphaMaxForcedProperty =
AvaloniaProperty.Register<ColorSlider, bool>(
nameof(IsAlphaMaxForced),
true);
/// <summary>
/// Defines the <see cref="IsAutoUpdatingEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsAutoUpdatingEnabledProperty =
AvaloniaProperty.Register<ColorSlider, bool>(
nameof(IsAutoUpdatingEnabled),
true);
/// <summary>
/// Defines the <see cref="IsSaturationValueMaxForced"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsSaturationValueMaxForcedProperty =
AvaloniaProperty.Register<ColorSlider, bool>(
nameof(IsSaturationValueMaxForced),
true);
/// <summary>
/// Gets or sets the currently selected color in the RGB color model.
/// </summary>
/// <remarks>
/// Use this property instead of <see cref="HsvColor"/> when in <see cref="ColorModel.Rgba"/>
/// to avoid loss of precision and color drifting.
/// </remarks>
public Color Color
{
get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
/// <summary>
/// Gets or sets the color component represented by the slider.
/// </summary>
public ColorComponent ColorComponent
{
get => GetValue(ColorComponentProperty);
set => SetValue(ColorComponentProperty, value);
}
/// <summary>
/// Gets or sets the active color model used by the slider.
/// </summary>
public ColorModel ColorModel
{
get => GetValue(ColorModelProperty);
set => SetValue(ColorModelProperty, value);
}
/// <summary>
/// Gets or sets the currently selected color in the HSV color model.
/// </summary>
/// <remarks>
/// Use this property instead of <see cref="Color"/> when in <see cref="ColorModel.Hsva"/>
/// to avoid loss of precision and color drifting.
/// </remarks>
public HsvColor HsvColor
{
get => GetValue(HsvColorProperty);
set => SetValue(HsvColorProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the alpha component is always forced to maximum for components
/// other than <see cref="ColorComponent"/>.
/// This ensures that the background is always visible and never transparent regardless of the actual color.
/// </summary>
public bool IsAlphaMaxForced
{
get => GetValue(IsAlphaMaxForcedProperty);
set => SetValue(IsAlphaMaxForcedProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether automatic background and foreground updates will be
/// calculated when the set color changes.
/// </summary>
/// <remarks>
/// This can be disabled for performance reasons when working with multiple sliders.
/// </remarks>
public bool IsAutoUpdatingEnabled
{
get => GetValue(IsAutoUpdatingEnabledProperty);
set => SetValue(IsAutoUpdatingEnabledProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values
/// when using the HSVA color model. Only component values other than <see cref="ColorComponent"/> will be changed.
/// This ensures, for example, that the Hue background is always visible and never washed out regardless of the actual color.
/// </summary>
public bool IsSaturationValueMaxForced
{
get => GetValue(IsSaturationValueMaxForcedProperty);
set => SetValue(IsSaturationValueMaxForcedProperty, value);
}
}
}

399
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs

@ -0,0 +1,399 @@
using System;
using Avalonia.Controls.Metadata;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// A slider with a background that represents a single color component.
/// </summary>
[PseudoClasses(pcDarkSelector, pcLightSelector)]
public partial class ColorSlider : Slider
{
protected const string pcDarkSelector = ":dark-selector";
protected const string pcLightSelector = ":light-selector";
/// <summary>
/// Event for when the selected color changes within the slider.
/// </summary>
public event EventHandler<ColorChangedEventArgs>? ColorChanged;
private const double MaxHue = 359.99999999999999999; // 17 decimal places
private bool disableUpdates = false;
/// <summary>
/// Initializes a new instance of the <see cref="ColorSlider"/> class.
/// </summary>
public ColorSlider() : base()
{
}
/// <summary>
/// Updates the visual state of the control by applying latest PseudoClasses.
/// </summary>
private void UpdatePseudoClasses()
{
// The slider itself can be transparent for certain color values.
// This causes an issue where a white selector thumb over a light window background or
// a black selector thumb over a dark window background is not visible.
// This means under a certain alpha threshold, neither a white or black selector thumb
// should be shown and instead the default slider thumb color should be used instead.
if (Color.A < 128 &&
(IsAlphaMaxForced == false ||
ColorComponent == ColorComponent.Alpha))
{
PseudoClasses.Set(pcDarkSelector, false);
PseudoClasses.Set(pcLightSelector, false);
}
else
{
Color perceivedColor;
if (ColorModel == ColorModel.Hsva)
{
perceivedColor = GetEquivalentBackgroundColor(HsvColor).ToRgb();
}
else
{
perceivedColor = GetEquivalentBackgroundColor(Color);
}
if (ColorHelper.GetRelativeLuminance(perceivedColor) <= 0.5)
{
PseudoClasses.Set(pcDarkSelector, false);
PseudoClasses.Set(pcLightSelector, true);
}
else
{
PseudoClasses.Set(pcDarkSelector, true);
PseudoClasses.Set(pcLightSelector, false);
}
}
}
/// <summary>
/// Generates a new background image for the color slider and applies it.
/// </summary>
private async void UpdateBackground()
{
// In Avalonia, Bounds returns the actual device-independent pixel size of a control.
// However, this is not necessarily the size of the control rendered on a display.
// A desktop or application scaling factor may be applied which must be accounted for here.
// Remember bitmaps in Avalonia are rendered mapping to actual device pixels, not the device-
// independent pixels of controls.
var scale = LayoutHelper.GetLayoutScale(this);
var pixelWidth = Convert.ToInt32(Bounds.Width * scale);
var pixelHeight = Convert.ToInt32(Bounds.Height * scale);
if (pixelWidth != 0 && pixelHeight != 0)
{
var bitmap = await ColorPickerHelpers.CreateComponentBitmapAsync(
pixelWidth,
pixelHeight,
Orientation,
ColorModel,
ColorComponent,
HsvColor,
IsAlphaMaxForced,
IsSaturationValueMaxForced);
if (bitmap != null)
{
Background = new ImageBrush(ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight));
}
}
}
/// <summary>
/// Updates the slider property values by applying the current color.
/// </summary>
/// <remarks>
/// Warning: This will trigger property changed updates.
/// Consider using <see cref="disableUpdates"/> externally.
/// </remarks>
private void SetColorToSliderValues()
{
var hsvColor = HsvColor;
var rgbColor = Color;
var component = ColorComponent;
if (ColorModel == ColorModel.Hsva)
{
// Note: Components converted into a usable range for the user
switch (component)
{
case ColorComponent.Alpha:
Minimum = 0;
Maximum = 100;
Value = hsvColor.A * 100;
break;
case ColorComponent.Component1: // Hue
Minimum = 0;
Maximum = MaxHue;
Value = hsvColor.H;
break;
case ColorComponent.Component2: // Saturation
Minimum = 0;
Maximum = 100;
Value = hsvColor.S * 100;
break;
case ColorComponent.Component3: // Value
Minimum = 0;
Maximum = 100;
Value = hsvColor.V * 100;
break;
}
}
else
{
switch (component)
{
case ColorComponent.Alpha:
Minimum = 0;
Maximum = 255;
Value = Convert.ToDouble(rgbColor.A);
break;
case ColorComponent.Component1: // Red
Minimum = 0;
Maximum = 255;
Value = Convert.ToDouble(rgbColor.R);
break;
case ColorComponent.Component2: // Green
Minimum = 0;
Maximum = 255;
Value = Convert.ToDouble(rgbColor.G);
break;
case ColorComponent.Component3: // Blue
Minimum = 0;
Maximum = 255;
Value = Convert.ToDouble(rgbColor.B);
break;
}
}
}
/// <summary>
/// Gets the current color determined by the slider values.
/// </summary>
private (Color, HsvColor) GetColorFromSliderValues()
{
HsvColor hsvColor = new HsvColor();
Color rgbColor = new Color();
double sliderPercent = Value / (Maximum - Minimum);
var baseHsvColor = HsvColor;
var baseRgbColor = Color;
var component = ColorComponent;
if (ColorModel == ColorModel.Hsva)
{
switch (component)
{
case ColorComponent.Alpha:
{
hsvColor = new HsvColor(sliderPercent, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V);
break;
}
case ColorComponent.Component1:
{
hsvColor = new HsvColor(baseHsvColor.A, sliderPercent * MaxHue, baseHsvColor.S, baseHsvColor.V);
break;
}
case ColorComponent.Component2:
{
hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, sliderPercent, baseHsvColor.V);
break;
}
case ColorComponent.Component3:
{
hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, sliderPercent);
break;
}
}
return (hsvColor.ToRgb(), hsvColor);
}
else
{
byte componentValue = Convert.ToByte(MathUtilities.Clamp(sliderPercent * 255, 0, 255));
switch (component)
{
case ColorComponent.Alpha:
rgbColor = new Color(componentValue, baseRgbColor.R, baseRgbColor.G, baseRgbColor.B);
break;
case ColorComponent.Component1:
rgbColor = new Color(baseRgbColor.A, componentValue, baseRgbColor.G, baseRgbColor.B);
break;
case ColorComponent.Component2:
rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, componentValue, baseRgbColor.B);
break;
case ColorComponent.Component3:
rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, baseRgbColor.G, componentValue);
break;
}
return (rgbColor, rgbColor.ToHsv());
}
}
/// <summary>
/// Gets the actual background color displayed for the given HSV color.
/// This can differ due to the effects of certain properties intended to improve perception.
/// </summary>
/// <param name="hsvColor">The actual color to get the equivalent background color for.</param>
/// <returns>The equivalent, perceived background color.</returns>
private HsvColor GetEquivalentBackgroundColor(HsvColor hsvColor)
{
var component = ColorComponent;
var isAlphaMaxForced = IsAlphaMaxForced;
var isSaturationValueMaxForced = IsSaturationValueMaxForced;
if (isAlphaMaxForced &&
component != ColorComponent.Alpha)
{
hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V);
}
switch (component)
{
case ColorComponent.Component1:
return new HsvColor(
hsvColor.A,
hsvColor.H,
isSaturationValueMaxForced ? 1.0 : hsvColor.S,
isSaturationValueMaxForced ? 1.0 : hsvColor.V);
case ColorComponent.Component2:
return new HsvColor(
hsvColor.A,
hsvColor.H,
hsvColor.S,
isSaturationValueMaxForced ? 1.0 : hsvColor.V);
case ColorComponent.Component3:
return new HsvColor(
hsvColor.A,
hsvColor.H,
isSaturationValueMaxForced ? 1.0 : hsvColor.S,
hsvColor.V);
default:
return hsvColor;
}
}
/// <summary>
/// Gets the actual background color displayed for the given RGB color.
/// This can differ due to the effects of certain properties intended to improve perception.
/// </summary>
/// <param name="rgbColor">The actual color to get the equivalent background color for.</param>
/// <returns>The equivalent, perceived background color.</returns>
private Color GetEquivalentBackgroundColor(Color rgbColor)
{
var component = ColorComponent;
var isAlphaMaxForced = IsAlphaMaxForced;
if (isAlphaMaxForced &&
component != ColorComponent.Alpha)
{
rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B);
}
return rgbColor;
}
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (disableUpdates)
{
base.OnPropertyChanged(change);
return;
}
// Always keep the two color properties in sync
if (change.Property == ColorProperty)
{
disableUpdates = true;
HsvColor = Color.ToHsv();
if (IsAutoUpdatingEnabled)
{
SetColorToSliderValues();
UpdateBackground();
}
UpdatePseudoClasses();
OnColorChanged(new ColorChangedEventArgs(
change.GetOldValue<Color>(),
change.GetNewValue<Color>()));
disableUpdates = false;
}
else if (change.Property == HsvColorProperty)
{
disableUpdates = true;
Color = HsvColor.ToRgb();
if (IsAutoUpdatingEnabled)
{
SetColorToSliderValues();
UpdateBackground();
}
UpdatePseudoClasses();
OnColorChanged(new ColorChangedEventArgs(
change.GetOldValue<HsvColor>().ToRgb(),
change.GetNewValue<HsvColor>().ToRgb()));
disableUpdates = false;
}
else if (change.Property == BoundsProperty)
{
if (IsAutoUpdatingEnabled)
{
UpdateBackground();
}
}
else if (change.Property == ValueProperty ||
change.Property == MinimumProperty ||
change.Property == MaximumProperty)
{
disableUpdates = true;
Color oldColor = Color;
(var color, var hsvColor) = GetColorFromSliderValues();
if (ColorModel == ColorModel.Hsva)
{
HsvColor = hsvColor;
Color = hsvColor.ToRgb();
}
else
{
Color = color;
HsvColor = color.ToHsv();
}
UpdatePseudoClasses();
OnColorChanged(new ColorChangedEventArgs(oldColor, Color));
disableUpdates = false;
}
base.OnPropertyChanged(change);
}
/// <summary>
/// Called before the <see cref="ColorChanged"/> event occurs.
/// </summary>
/// <param name="e">The <see cref="ColorChangedEventArgs"/> defining old/new colors.</param>
protected virtual void OnColorChanged(ColorChangedEventArgs e)
{
ColorChanged?.Invoke(this, e);
}
}
}

222
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs

@ -0,0 +1,222 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using Avalonia.Data;
using Avalonia.Media;
namespace Avalonia.Controls.Primitives
{
/// <inheritdoc/>
public partial class ColorSpectrum
{
/// <summary>
/// Defines the <see cref="Color"/> property.
/// </summary>
public static readonly StyledProperty<Color> ColorProperty =
AvaloniaProperty.Register<ColorSpectrum, Color>(
nameof(Color),
Colors.White,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="Components"/> property.
/// </summary>
public static readonly StyledProperty<ColorSpectrumComponents> ComponentsProperty =
AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumComponents>(
nameof(Components),
ColorSpectrumComponents.HueSaturation);
/// <summary>
/// Defines the <see cref="HsvColor"/> property.
/// </summary>
public static readonly StyledProperty<HsvColor> HsvColorProperty =
AvaloniaProperty.Register<ColorSpectrum, HsvColor>(
nameof(HsvColor),
Colors.White.ToHsv(),
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="MaxHue"/> property.
/// </summary>
public static readonly StyledProperty<int> MaxHueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(
nameof(MaxHue),
359);
/// <summary>
/// Defines the <see cref="MaxSaturation"/> property.
/// </summary>
public static readonly StyledProperty<int> MaxSaturationProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(
nameof(MaxSaturation),
100);
/// <summary>
/// Defines the <see cref="MaxValue"/> property.
/// </summary>
public static readonly StyledProperty<int> MaxValueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(
nameof(MaxValue),
100);
/// <summary>
/// Defines the <see cref="MinHue"/> property.
/// </summary>
public static readonly StyledProperty<int> MinHueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(
nameof(MinHue),
0);
/// <summary>
/// Defines the <see cref="MinSaturation"/> property.
/// </summary>
public static readonly StyledProperty<int> MinSaturationProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(
nameof(MinSaturation),
0);
/// <summary>
/// Defines the <see cref="MinValue"/> property.
/// </summary>
public static readonly StyledProperty<int> MinValueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(
nameof(MinValue),
0);
/// <summary>
/// Defines the <see cref="Shape"/> property.
/// </summary>
public static readonly StyledProperty<ColorSpectrumShape> ShapeProperty =
AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumShape>(
nameof(Shape),
ColorSpectrumShape.Box);
/// <summary>
/// Gets or sets the currently selected color in the RGB color model.
/// </summary>
/// <remarks>
/// For control authors use <see cref="HsvColor"/> instead to avoid loss
/// of precision and color drifting.
/// </remarks>
public Color Color
{
get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
/// <summary>
/// Gets or sets the two HSV color components displayed by the spectrum.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public ColorSpectrumComponents Components
{
get => GetValue(ComponentsProperty);
set => SetValue(ComponentsProperty, value);
}
/// <summary>
/// Gets or sets the currently selected color in the HSV color model.
/// </summary>
/// <remarks>
/// This should be used in all cases instead of the <see cref="Color"/> property.
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model and using
/// this property will avoid loss of precision and color drifting.
/// </remarks>
public HsvColor HsvColor
{
get => GetValue(HsvColorProperty);
set => SetValue(HsvColorProperty, value);
}
/// <summary>
/// Gets or sets the maximum value of the Hue component in the range from 0..359.
/// This property must be greater than <see cref="MinHue"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MaxHue
{
get => GetValue(MaxHueProperty);
set => SetValue(MaxHueProperty, value);
}
/// <summary>
/// Gets or sets the maximum value of the Saturation component in the range from 0..100.
/// This property must be greater than <see cref="MinSaturation"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MaxSaturation
{
get => GetValue(MaxSaturationProperty);
set => SetValue(MaxSaturationProperty, value);
}
/// <summary>
/// Gets or sets the maximum value of the Value component in the range from 0..100.
/// This property must be greater than <see cref="MinValue"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MaxValue
{
get => GetValue(MaxValueProperty);
set => SetValue(MaxValueProperty, value);
}
/// <summary>
/// Gets or sets the minimum value of the Hue component in the range from 0..359.
/// This property must be less than <see cref="MaxHue"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MinHue
{
get => GetValue(MinHueProperty);
set => SetValue(MinHueProperty, value);
}
/// <summary>
/// Gets or sets the minimum value of the Saturation component in the range from 0..100.
/// This property must be less than <see cref="MaxSaturation"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MinSaturation
{
get => GetValue(MinSaturationProperty);
set => SetValue(MinSaturationProperty, value);
}
/// <summary>
/// Gets or sets the minimum value of the Value component in the range from 0..100.
/// This property must be less than <see cref="MaxValue"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MinValue
{
get => GetValue(MinValueProperty);
set => SetValue(MinValueProperty, value);
}
/// <summary>
/// Gets or sets the displayed shape of the spectrum.
/// </summary>
public ColorSpectrumShape Shape
{
get => GetValue(ShapeProperty);
set => SetValue(ShapeProperty, value);
}
}
}

1606
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs

File diff suppressed because it is too large

73
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs

@ -0,0 +1,73 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls
{
/// <summary>
/// Defines the two HSV color components displayed by a <see cref="ColorSpectrum"/>.
/// </summary>
/// <remarks>
/// Order of the color components is important and correspond with an X/Y axis in Box
/// shape or a degree/radius in Ring shape.
/// </remarks>
public enum ColorSpectrumComponents
{
/// <summary>
/// The Hue and Value components.
/// </summary>
/// <remarks>
/// In Box shape, Hue is mapped to the X-axis and Value is mapped to the Y-axis.
/// In Ring shape, Hue is mapped to degrees and Value is mapped to radius.
/// </remarks>
HueValue,
/// <summary>
/// The Value and Hue components.
/// </summary>
/// <remarks>
/// In Box shape, Value is mapped to the X-axis and Hue is mapped to the Y-axis.
/// In Ring shape, Value is mapped to degrees and Hue is mapped to radius.
/// </remarks>
ValueHue,
/// <summary>
/// The Hue and Saturation components.
/// </summary>
/// <remarks>
/// In Box shape, Hue is mapped to the X-axis and Saturation is mapped to the Y-axis.
/// In Ring shape, Hue is mapped to degrees and Saturation is mapped to radius.
/// </remarks>
HueSaturation,
/// <summary>
/// The Saturation and Hue components.
/// </summary>
/// <remarks>
/// In Box shape, Saturation is mapped to the X-axis and Hue is mapped to the Y-axis.
/// In Ring shape, Saturation is mapped to degrees and Hue is mapped to radius.
/// </remarks>
SaturationHue,
/// <summary>
/// The Saturation and Value components.
/// </summary>
/// <remarks>
/// In Box shape, Saturation is mapped to the X-axis and Value is mapped to the Y-axis.
/// In Ring shape, Saturation is mapped to degrees and Value is mapped to radius.
/// </remarks>
SaturationValue,
/// <summary>
/// The Value and Saturation components.
/// </summary>
/// <remarks>
/// In Box shape, Value is mapped to the X-axis and Saturation is mapped to the Y-axis.
/// In Ring shape, Value is mapped to degrees and Saturation is mapped to radius.
/// </remarks>
ValueSaturation,
};
}

26
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs

@ -0,0 +1,26 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls
{
/// <summary>
/// Defines the shape of a <see cref="ColorSpectrum"/>.
/// </summary>
public enum ColorSpectrumShape
{
/// <summary>
/// The spectrum is in the shape of a rectangular or square box.
/// Note that more colors are visible to the user in Box shape.
/// </summary>
Box,
/// <summary>
/// The spectrum is in the shape of an ellipse or circle.
/// </summary>
Ring,
};
}

116
src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs

@ -0,0 +1,116 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Controls.Primitives.Converters
{
/// <summary>
/// Creates an accent color for a given base color value and step parameter.
/// This is a highly-specialized converter for the color picker.
/// </summary>
public class AccentColorConverter : IValueConverter
{
/// <summary>
/// The amount to change the Value component for each accent color step.
/// </summary>
public const double ValueDelta = 0.1;
/// <inheritdoc/>
public object? Convert(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
int accentStep;
Color? rgbColor = null;
HsvColor? hsvColor = null;
if (value is Color valueColor)
{
rgbColor = valueColor;
}
else if (value is HslColor valueHslColor)
{
rgbColor = valueHslColor.ToRgb();
}
else if (value is HsvColor valueHsvColor)
{
hsvColor = valueHsvColor;
}
else if (value is SolidColorBrush valueBrush)
{
rgbColor = valueBrush.Color;
}
else
{
// Invalid color value provided
return AvaloniaProperty.UnsetValue;
}
// Get the value component delta
try
{
accentStep = int.Parse(parameter?.ToString() ?? "", CultureInfo.InvariantCulture);
}
catch
{
// Invalid parameter provided, unable to convert to integer
return AvaloniaProperty.UnsetValue;
}
if (hsvColor == null &&
rgbColor != null)
{
hsvColor = rgbColor.Value.ToHsv();
}
if (hsvColor != null)
{
return new SolidColorBrush(GetAccent(hsvColor.Value, accentStep).ToRgb());
}
else
{
return AvaloniaProperty.UnsetValue;
}
}
/// <inheritdoc/>
public object? ConvertBack(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
return AvaloniaProperty.UnsetValue;
}
/// <summary>
/// This does not account for perceptual differences and also does not match with
/// system accent color calculation.
/// </summary>
/// <remarks>
/// Use the HSV representation as it's more perceptual.
/// In most cases only the value is changed by a fixed percentage so the algorithm is reproducible.
/// </remarks>
/// <param name="hsvColor">The base color to calculate the accent from.</param>
/// <param name="accentStep">The number of accent color steps to move.</param>
/// <returns>The new accent color.</returns>
public static HsvColor GetAccent(HsvColor hsvColor, int accentStep)
{
if (accentStep != 0)
{
double colorValue = hsvColor.V;
colorValue += (accentStep * AccentColorConverter.ValueDelta);
colorValue = Math.Round(colorValue, 2);
return new HsvColor(hsvColor.A, hsvColor.H, hsvColor.S, colorValue);
}
else
{
return hsvColor;
}
}
}
}

68
src/Avalonia.Controls.ColorPicker/Converters/ColorToDisplayNameConverter.cs

@ -0,0 +1,68 @@
using System;
using System.Globalization;
using Avalonia.Controls.Primitives;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Gets the approximated display name for the color.
/// </summary>
public class ColorToDisplayNameConverter : IValueConverter
{
/// <inheritdoc/>
public object? Convert(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
Color color;
if (value is Color valueColor)
{
color = valueColor;
}
else if (value is HslColor valueHslColor)
{
color = valueHslColor.ToRgb();
}
else if (value is HsvColor valueHsvColor)
{
color = valueHsvColor.ToRgb();
}
else if (value is SolidColorBrush valueBrush)
{
color = valueBrush.Color;
}
else
{
// Invalid color value provided
return AvaloniaProperty.UnsetValue;
}
// ColorHelper.ToDisplayName ignores the alpha component
// This means fully transparent colors will be named as a real color
// That undesirable behavior is specially overridden here
if (color.A == 0x00)
{
return AvaloniaProperty.UnsetValue;
}
else
{
return ColorHelper.ToDisplayName(color);
}
}
/// <inheritdoc/>
public object? ConvertBack(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
return AvaloniaProperty.UnsetValue;
}
}
}

82
src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs

@ -0,0 +1,82 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converts a color to a hex string and vice versa.
/// </summary>
public class ColorToHexConverter : IValueConverter
{
/// <inheritdoc/>
public object? Convert(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
Color color;
bool includeSymbol = parameter as bool? ?? false;
if (value is Color valueColor)
{
color = valueColor;
}
else if (value is HslColor valueHslColor)
{
color = valueHslColor.ToRgb();
}
else if (value is HsvColor valueHsvColor)
{
color = valueHsvColor.ToRgb();
}
else if (value is SolidColorBrush valueBrush)
{
color = valueBrush.Color;
}
else
{
// Invalid color value provided
return AvaloniaProperty.UnsetValue;
}
string hexColor = color.ToString();
if (includeSymbol == false)
{
// TODO: When .net standard 2.0 is dropped, replace the below line
//hexColor = hexColor.Replace("#", string.Empty, StringComparison.Ordinal);
hexColor = hexColor.Replace("#", string.Empty);
}
return hexColor;
}
/// <inheritdoc/>
public object? ConvertBack(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
string hexValue = value?.ToString() ?? string.Empty;
if (Color.TryParse(hexValue, out Color color))
{
return color;
}
else if (hexValue.StartsWith("#", StringComparison.Ordinal) == false &&
Color.TryParse("#" + hexValue, out Color color2))
{
return color2;
}
else
{
// Invalid hex color value provided
return AvaloniaProperty.UnsetValue;
}
}
}
}

51
src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs

@ -0,0 +1,51 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Avalonia.Controls.Primitives.Converters
{
/// <summary>
/// Gets the third <see cref="ColorComponent"/> corresponding with a given
/// <see cref="ColorSpectrumComponents"/> that represents the other two components.
/// This is a highly-specialized converter for the color picker.
/// </summary>
public class ThirdComponentConverter : IValueConverter
{
/// <inheritdoc/>
public object? Convert(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
if (value is ColorSpectrumComponents components)
{
// Note: Alpha is not relevant here
switch (components)
{
case ColorSpectrumComponents.HueSaturation:
case ColorSpectrumComponents.SaturationHue:
return (ColorComponent)HsvComponent.Value;
case ColorSpectrumComponents.HueValue:
case ColorSpectrumComponents.ValueHue:
return (ColorComponent)HsvComponent.Saturation;
case ColorSpectrumComponents.SaturationValue:
case ColorSpectrumComponents.ValueSaturation:
return (ColorComponent)HsvComponent.Hue;
}
}
return AvaloniaProperty.UnsetValue;
}
/// <inheritdoc/>
public object? ConvertBack(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
return AvaloniaProperty.UnsetValue;
}
}
}

50
src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs

@ -0,0 +1,50 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converts the given value into an <see cref="IBrush"/> when a conversion is possible.
/// </summary>
public class ToBrushConverter : IValueConverter
{
/// <inheritdoc/>
public object? Convert(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
if (value is IBrush brush)
{
return brush;
}
else if (value is Color valueColor)
{
return new SolidColorBrush(valueColor);
}
else if (value is HslColor valueHslColor)
{
return new SolidColorBrush(valueHslColor.ToRgb());
}
else if (value is HsvColor valueHsvColor)
{
return new SolidColorBrush(valueHsvColor.ToRgb());
}
return AvaloniaProperty.UnsetValue;
}
/// <inheritdoc/>
public object? ConvertBack(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
return AvaloniaProperty.UnsetValue;
}
}
}

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

Loading…
Cancel
Save