Browse Source

Merge branch 'master' into adorner-layer-canvas

pull/8075/head
Luis v.d.Eltz 4 years ago
committed by GitHub
parent
commit
d48181cd76
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      native/Avalonia.Native/src/OSX/AutoFitContentView.h
  2. 104
      native/Avalonia.Native/src/OSX/AutoFitContentView.mm
  3. 48
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  4. 6
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme
  5. 29
      native/Avalonia.Native/src/OSX/AvnView.h
  6. 706
      native/Avalonia.Native/src/OSX/AvnView.mm
  7. 17
      native/Avalonia.Native/src/OSX/INSWindowHolder.h
  8. 18
      native/Avalonia.Native/src/OSX/IWindowStateChanged.h
  9. 25
      native/Avalonia.Native/src/OSX/ResizeScope.h
  10. 18
      native/Avalonia.Native/src/OSX/ResizeScope.mm
  11. 1
      native/Avalonia.Native/src/OSX/SystemDialogs.mm
  12. 121
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  13. 505
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  14. 97
      native/Avalonia.Native/src/OSX/WindowImpl.h
  15. 539
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  16. 12
      native/Avalonia.Native/src/OSX/app.mm
  17. 1
      native/Avalonia.Native/src/OSX/automation.h
  18. 6
      native/Avalonia.Native/src/OSX/automation.mm
  19. 3
      native/Avalonia.Native/src/OSX/common.h
  20. 1
      native/Avalonia.Native/src/OSX/cursor.mm
  21. 6
      native/Avalonia.Native/src/OSX/main.mm
  22. 1
      native/Avalonia.Native/src/OSX/menu.h
  23. 7
      native/Avalonia.Native/src/OSX/menu.mm
  24. 4
      native/Avalonia.Native/src/OSX/rendertarget.mm
  25. 61
      native/Avalonia.Native/src/OSX/window.h
  26. 2129
      native/Avalonia.Native/src/OSX/window.mm
  27. 4
      samples/ControlCatalog/App.xaml.cs
  28. 86
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  29. 10
      samples/ControlCatalog/Pages/ComboBoxPage.xaml
  30. 11
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  31. 51
      src/Avalonia.Base/Logging/ILogSink.cs
  32. 38
      src/Avalonia.Base/Logging/TraceLogSink.cs
  33. 6
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  34. 6
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  35. 91
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  36. 2
      src/Avalonia.Base/Styling/IStyle.cs
  37. 2
      src/Avalonia.Base/Styling/Styles.cs
  38. 2
      src/Avalonia.Base/Utilities/WeakHashList.cs
  39. 28
      src/Avalonia.Controls.ColorPicker/ColorComponent.cs
  40. 18
      src/Avalonia.Controls.ColorPicker/ColorModel.cs
  41. 50
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
  42. 130
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
  43. 146
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
  44. 399
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
  45. 414
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs
  46. 151
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  47. 138
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  48. 0
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs
  49. 0
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs
  50. 116
      src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs
  51. 68
      src/Avalonia.Controls.ColorPicker/Converters/ColorToDisplayNameConverter.cs
  52. 82
      src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
  53. 51
      src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs
  54. 50
      src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs
  55. 58
      src/Avalonia.Controls.ColorPicker/Converters/ToColorConverter.cs
  56. 50
      src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs
  57. 142
      src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs
  58. 629
      src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs
  59. 0
      src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs
  60. 0
      src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs
  61. 0
      src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs
  62. 0
      src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs
  63. 22
      src/Avalonia.Controls.ColorPicker/HsvComponent.cs
  64. 42
      src/Avalonia.Controls.ColorPicker/RgbComponent.cs
  65. 86
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml
  66. 194
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml
  67. 22
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml
  68. 28
      src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml
  69. 86
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
  70. 194
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml
  71. 25
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml
  72. 28
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml
  73. 15
      src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs
  74. 25
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  75. 56
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  76. 42
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  77. 150
      src/Avalonia.Controls/Primitives/Popup.cs
  78. 39
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  79. 19
      src/Avalonia.Controls/TextBlock.cs
  80. 64
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  81. 41
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  82. 10
      src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml
  83. 3
      src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml
  84. 18
      src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml
  85. 22
      src/Avalonia.Themes.Default/Controls/PopupRoot.xaml
  86. 28
      src/Avalonia.Themes.Default/IBitmapToImageConverter.cs
  87. 3
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  88. 7
      src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
  89. 16
      src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml
  90. 14
      src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml
  91. 28
      src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs
  92. 18
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
  93. 6
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  94. 1
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  95. 1
      src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs
  96. 7
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  97. 33
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  98. 4
      src/iOS/Avalonia.iOS/TouchHandler.cs
  99. 8
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  100. 4
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

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

@ -0,0 +1,15 @@
//
// Created by Dan Walmsley on 05/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "avalonia-native.h"
@interface AutoFitContentView : NSView
-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content;
-(void) ShowTitleBar: (bool) show;
-(void) SetTitleBarHeightHint: (double) height;
-(void) ShowBlur: (bool) show;
@end

104
native/Avalonia.Native/src/OSX/AutoFitContentView.mm

@ -0,0 +1,104 @@
//
// Created by Dan Walmsley on 05/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#include "AvnView.h"
#import "AutoFitContentView.h"
@implementation AutoFitContentView
{
NSVisualEffectView* _titleBarMaterial;
NSBox* _titleBarUnderline;
NSView* _content;
NSVisualEffectView* _blurBehind;
double _titleBarHeightHint;
bool _settingSize;
}
-(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content
{
_titleBarHeightHint = -1;
_content = content;
_settingSize = false;
[self setAutoresizesSubviews:true];
[self setWantsLayer:true];
_titleBarMaterial = [NSVisualEffectView new];
[_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow];
[_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar];
[_titleBarMaterial setWantsLayer:true];
_titleBarMaterial.hidden = true;
_titleBarUnderline = [NSBox new];
_titleBarUnderline.boxType = NSBoxSeparator;
_titleBarUnderline.fillColor = [NSColor underPageBackgroundColor];
_titleBarUnderline.hidden = true;
[self addSubview:_titleBarMaterial];
[self addSubview:_titleBarUnderline];
_blurBehind = [NSVisualEffectView new];
[_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow];
[_blurBehind setMaterial:NSVisualEffectMaterialLight];
[_blurBehind setWantsLayer:true];
_blurBehind.hidden = true;
[_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
[_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
[self addSubview:_blurBehind];
[self addSubview:_content];
[self setWantsLayer:true];
return self;
}
-(void) ShowBlur:(bool)show
{
_blurBehind.hidden = !show;
}
-(void) ShowTitleBar: (bool) show
{
_titleBarMaterial.hidden = !show;
_titleBarUnderline.hidden = !show;
}
-(void) SetTitleBarHeightHint: (double) height
{
_titleBarHeightHint = height;
[self setFrameSize:self.frame.size];
}
-(void)setFrameSize:(NSSize)newSize
{
if(_settingSize)
{
return;
}
_settingSize = true;
[super setFrameSize:newSize];
auto window = objc_cast<AvnWindow>([self window]);
// TODO get actual titlebar size
double height = _titleBarHeightHint == -1 ? [window getExtendedTitleBarHeight] : _titleBarHeightHint;
NSRect tbar;
tbar.origin.x = 0;
tbar.origin.y = newSize.height - height;
tbar.size.width = newSize.width;
tbar.size.height = height;
[_titleBarMaterial setFrame:tbar];
tbar.size.height = height < 1 ? 0 : 1;
[_titleBarUnderline setFrame:tbar];
_settingSize = false;
}
@end

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

@ -7,6 +7,18 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; };
18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839166350F32661F3ABD70F /* AutoFitContentView.mm */; };
18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839171D898F9BFC1373631A /* ResizeScope.h */; };
18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */ = {isa = PBXBuildFile; fileRef = 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */; };
18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839132D0E2454D911F1D1F9 /* AvnView.mm */; };
18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */; };
18391ED5F611FF62C45F196D /* AvnView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391D1669284AD2EC9E866A /* AvnView.h */; };
1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; };
1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; }; 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; };
1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; }; 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; };
@ -35,6 +47,18 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
1839132D0E2454D911F1D1F9 /* AvnView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnView.mm; sourceTree = "<group>"; };
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>"; };
18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoFitContentView.h; sourceTree = "<group>"; };
1839166350F32661F3ABD70F /* AutoFitContentView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AutoFitContentView.mm; 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>"; };
18391D1669284AD2EC9E866A /* AvnView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvnView.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>"; }; 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>"; }; 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>"; }; 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = "<group>"; };
@ -130,6 +154,18 @@
37C09D8721580FE4006A6758 /* SystemDialogs.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */,
AB7A61F02147C815003C5833 /* Products */, AB7A61F02147C815003C5833 /* Products */,
AB661C1C2148230E00291242 /* Frameworks */, 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 */,
1839132D0E2454D911F1D1F9 /* AvnView.mm */,
18391D1669284AD2EC9E866A /* AvnView.h */,
1839166350F32661F3ABD70F /* AutoFitContentView.mm */,
18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -150,6 +186,13 @@
files = ( files = (
37155CE4233C00EB0034DCE9 /* menu.h in Headers */, 37155CE4233C00EB0034DCE9 /* menu.h in Headers */,
BC11A5BE2608D58F0017BAD0 /* automation.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 */,
18391ED5F611FF62C45F196D /* AvnView.h in Headers */,
18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -229,6 +272,11 @@
AB00E4F72147CA920032A60A /* main.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */,
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */,
AB661C202148286E00291242 /* window.mm in Sources */, AB661C202148286E00291242 /* window.mm in Sources */,
1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */,
1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */,
18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */,
18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */,
18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

6
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme

@ -56,10 +56,14 @@
</MacroExpansion> </MacroExpansion>
<CommandLineArguments> <CommandLineArguments>
<CommandLineArgument <CommandLineArgument
argument = "bin/Debug/netcoreapp3.1/ControlCatalog.NetCore.dll" argument = "bin/Debug/net6.0/ControlCatalog.NetCore.dll"
isEnabled = "YES"> isEnabled = "YES">
</CommandLineArgument> </CommandLineArgument>
</CommandLineArguments> </CommandLineArguments>
<LocationScenarioReference
identifier = "com.apple.dt.IDEFoundation.CurrentLocationScenarioIdentifier"
referenceType = "1">
</LocationScenarioReference>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

29
native/Avalonia.Native/src/OSX/AvnView.h

@ -0,0 +1,29 @@
//
// Created by Dan Walmsley on 05/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include "window.h"
#import "comimpl.h"
#import "common.h"
#import "WindowImpl.h"
#import "KeyTransform.h"
@class AvnAccessibilityElement;
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(NSEvent* _Nonnull) lastMouseDownEvent;
-(AvnPoint) translateLocalPoint:(AvnPoint)pt;
-(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose;
-(void) onClosed;
-(AvnPlatformResizeReason) getResizeReason;
-(void) setResizeReason:(AvnPlatformResizeReason)reason;
+ (AvnPoint)toAvnPoint:(CGPoint)p;
@end

706
native/Avalonia.Native/src/OSX/AvnView.mm

@ -0,0 +1,706 @@
//
// Created by Dan Walmsley on 05/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#import <AppKit/AppKit.h>
#import "AvnView.h"
#include "automation.h"
@implementation AvnView
{
ComPtr<WindowBaseImpl> _parent;
NSTrackingArea* _area;
bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed;
AvnInputModifiers _modifierState;
NSEvent* _lastMouseDownEvent;
bool _lastKeyHandled;
AvnPixelSize _lastPixelSize;
NSObject<IRenderTarget>* _renderTarget;
AvnPlatformResizeReason _resizeReason;
AvnAccessibilityElement* _accessibilityChild;
}
- (void)onClosed
{
@synchronized (self)
{
_parent = nullptr;
}
}
- (NSEvent*) lastMouseDownEvent
{
return _lastMouseDownEvent;
}
- (void) updateRenderTarget
{
[_renderTarget resize:_lastPixelSize withScale:static_cast<float>([[self window] backingScaleFactor])];
[self setNeedsDisplayInRect:[self frame]];
}
-(AvnView*) initWithParent: (WindowBaseImpl*) parent
{
self = [super init];
_renderTarget = parent->renderTarget;
[self setWantsLayer:YES];
[self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize];
_parent = parent;
_area = nullptr;
_lastPixelSize.Height = 100;
_lastPixelSize.Width = 100;
[self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]];
_modifierState = AvnInputModifiersNone;
return self;
}
- (BOOL)isFlipped
{
return YES;
}
- (BOOL)wantsUpdateLayer
{
return YES;
}
- (void)setLayer:(CALayer *)layer
{
[_renderTarget setNewLayer: layer];
[super setLayer: layer];
}
- (BOOL)isOpaque
{
return YES;
}
- (BOOL)acceptsFirstResponder
{
return true;
}
- (BOOL)acceptsFirstMouse:(NSEvent *)event
{
return true;
}
- (BOOL)canBecomeKeyView
{
return true;
}
-(void)setFrameSize:(NSSize)newSize
{
[super setFrameSize:newSize];
if(_area != nullptr)
{
[self removeTrackingArea:_area];
_area = nullptr;
}
if (_parent == nullptr)
{
return;
}
NSRect rect = NSZeroRect;
rect.size = newSize;
NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingEnabledDuringMouseDrag;
_area = [[NSTrackingArea alloc] initWithRect:rect options:options owner:self userInfo:nullptr];
[self addTrackingArea:_area];
_parent->UpdateCursor();
auto fsize = [self convertSizeToBacking: [self frame].size];
if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height)
{
_lastPixelSize.Width = (int)fsize.width;
_lastPixelSize.Height = (int)fsize.height;
[self updateRenderTarget];
auto reason = [self inLiveResize] ? ResizeUser : _resizeReason;
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
}
}
- (void)updateLayer
{
AvnInsidePotentialDeadlock deadlock;
if (_parent == nullptr)
{
return;
}
_parent->BaseEvents->RunRenderPriorityJobs();
if (_parent == nullptr)
{
return;
}
_parent->BaseEvents->Paint();
}
- (void)drawRect:(NSRect)dirtyRect
{
return;
}
-(void) setSwRenderedFrame: (AvnFramebuffer*) fb dispose: (IUnknown*) dispose
{
@autoreleasepool {
[_renderTarget setSwFrame:fb];
dispose->Release();
}
}
- (AvnPoint) translateLocalPoint:(AvnPoint)pt
{
pt.Y = [self bounds].size.height - pt.Y;
return pt;
}
+ (AvnPoint)toAvnPoint:(CGPoint)p
{
AvnPoint result;
result.X = p.x;
result.Y = p.y;
return result;
}
- (void) viewDidChangeBackingProperties
{
auto fsize = [self convertSizeToBacking: [self frame].size];
_lastPixelSize.Width = (int)fsize.width;
_lastPixelSize.Height = (int)fsize.height;
[self updateRenderTarget];
if(_parent != nullptr)
{
_parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]);
}
[super viewDidChangeBackingProperties];
}
- (bool) ignoreUserInput:(bool)trigerInputWhenDisabled
{
auto parentWindow = objc_cast<AvnWindow>([self window]);
if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents])
{
if(trigerInputWhenDisabled)
{
auto window = dynamic_cast<WindowImpl*>(_parent.getRaw());
if(window != nullptr)
{
window->WindowEvents->GotInputWhenDisabled();
}
}
return TRUE;
}
return FALSE;
}
- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type
{
bool triggerInputWhenDisabled = type != Move;
if([self ignoreUserInput: triggerInputWhenDisabled])
{
return;
}
auto localPoint = [self convertPoint:[event locationInWindow] toView:self];
auto avnPoint = [AvnView toAvnPoint:localPoint];
auto point = [self translateLocalPoint:avnPoint];
AvnVector delta = { 0, 0};
if(type == Wheel)
{
auto speed = 5;
if([event hasPreciseScrollingDeltas])
{
speed = 50;
}
delta.X = [event scrollingDeltaX] / speed;
delta.Y = [event scrollingDeltaY] / speed;
if(delta.X == 0 && delta.Y == 0)
{
return;
}
}
else if (type == Magnify)
{
delta.X = delta.Y = [event magnification];
}
else if (type == Rotate)
{
delta.X = delta.Y = [event rotation];
}
else if (type == Swipe)
{
delta.X = [event deltaX];
delta.Y = [event deltaY];
}
uint32 timestamp = static_cast<uint32>([event timestamp] * 1000);
auto modifiers = [self getModifiers:[event modifierFlags]];
if(type != Move ||
(
[self window] != nil &&
(
[[self window] firstResponder] == nil
|| ![[[self window] firstResponder] isKindOfClass: [NSView class]]
)
)
)
[self becomeFirstResponder];
if(_parent != nullptr)
{
_parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta);
}
[super mouseMoved:event];
}
- (BOOL) resignFirstResponder
{
_parent->BaseEvents->LostFocus();
return YES;
}
- (void)mouseMoved:(NSEvent *)event
{
[self mouseEvent:event withType:Move];
}
- (void)mouseDown:(NSEvent *)event
{
_isLeftPressed = true;
_lastMouseDownEvent = event;
[self mouseEvent:event withType:LeftButtonDown];
}
- (void)otherMouseDown:(NSEvent *)event
{
_lastMouseDownEvent = event;
switch(event.buttonNumber)
{
case 2:
case 3:
_isMiddlePressed = true;
[self mouseEvent:event withType:MiddleButtonDown];
break;
case 4:
_isXButton1Pressed = true;
[self mouseEvent:event withType:XButton1Down];
break;
case 5:
_isXButton2Pressed = true;
[self mouseEvent:event withType:XButton2Down];
break;
default:
break;
}
}
- (void)rightMouseDown:(NSEvent *)event
{
_isRightPressed = true;
_lastMouseDownEvent = event;
[self mouseEvent:event withType:RightButtonDown];
}
- (void)mouseUp:(NSEvent *)event
{
_isLeftPressed = false;
[self mouseEvent:event withType:LeftButtonUp];
}
- (void)otherMouseUp:(NSEvent *)event
{
switch(event.buttonNumber)
{
case 2:
case 3:
_isMiddlePressed = false;
[self mouseEvent:event withType:MiddleButtonUp];
break;
case 4:
_isXButton1Pressed = false;
[self mouseEvent:event withType:XButton1Up];
break;
case 5:
_isXButton2Pressed = false;
[self mouseEvent:event withType:XButton2Up];
break;
default:
break;
}
}
- (void)rightMouseUp:(NSEvent *)event
{
_isRightPressed = false;
[self mouseEvent:event withType:RightButtonUp];
}
- (void)mouseDragged:(NSEvent *)event
{
[self mouseEvent:event withType:Move];
[super mouseDragged:event];
}
- (void)otherMouseDragged:(NSEvent *)event
{
[self mouseEvent:event withType:Move];
[super otherMouseDragged:event];
}
- (void)rightMouseDragged:(NSEvent *)event
{
[self mouseEvent:event withType:Move];
[super rightMouseDragged:event];
}
- (void)scrollWheel:(NSEvent *)event
{
[self mouseEvent:event withType:Wheel];
[super scrollWheel:event];
}
- (void)magnifyWithEvent:(NSEvent *)event
{
[self mouseEvent:event withType:Magnify];
[super magnifyWithEvent:event];
}
- (void)rotateWithEvent:(NSEvent *)event
{
[self mouseEvent:event withType:Rotate];
[super rotateWithEvent:event];
}
- (void)swipeWithEvent:(NSEvent *)event
{
[self mouseEvent:event withType:Swipe];
[super swipeWithEvent:event];
}
- (void)mouseEntered:(NSEvent *)event
{
[super mouseEntered:event];
}
- (void)mouseExited:(NSEvent *)event
{
[self mouseEvent:event withType:LeaveWindow];
[super mouseExited:event];
}
- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type
{
if([self ignoreUserInput: false])
{
return;
}
auto key = s_KeyMap[[event keyCode]];
uint32_t timestamp = static_cast<uint32_t>([event timestamp] * 1000);
auto modifiers = [self getModifiers:[event modifierFlags]];
if(_parent != nullptr)
{
_lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key);
}
}
- (BOOL)performKeyEquivalent:(NSEvent *)event
{
bool result = _lastKeyHandled;
_lastKeyHandled = false;
return result;
}
- (void)flagsChanged:(NSEvent *)event
{
auto newModifierState = [self getModifiers:[event modifierFlags]];
bool isAltCurrentlyPressed = (_modifierState & Alt) == Alt;
bool isControlCurrentlyPressed = (_modifierState & Control) == Control;
bool isShiftCurrentlyPressed = (_modifierState & Shift) == Shift;
bool isCommandCurrentlyPressed = (_modifierState & Windows) == Windows;
bool isAltPressed = (newModifierState & Alt) == Alt;
bool isControlPressed = (newModifierState & Control) == Control;
bool isShiftPressed = (newModifierState & Shift) == Shift;
bool isCommandPressed = (newModifierState & Windows) == Windows;
if (isAltPressed && !isAltCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if (isAltCurrentlyPressed && !isAltPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if (isControlPressed && !isControlCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if (isControlCurrentlyPressed && !isControlPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if (isShiftPressed && !isShiftCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if(isShiftCurrentlyPressed && !isShiftPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if(isCommandPressed && !isCommandCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if(isCommandCurrentlyPressed && ! isCommandPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
_modifierState = newModifierState;
[[self inputContext] handleEvent:event];
[super flagsChanged:event];
}
- (void)keyDown:(NSEvent *)event
{
[self keyboardEvent:event withType:KeyDown];
[[self inputContext] handleEvent:event];
[super keyDown:event];
}
- (void)keyUp:(NSEvent *)event
{
[self keyboardEvent:event withType:KeyUp];
[super keyUp:event];
}
- (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod
{
unsigned int rv = 0;
if (mod & NSEventModifierFlagControl)
rv |= Control;
if (mod & NSEventModifierFlagShift)
rv |= Shift;
if (mod & NSEventModifierFlagOption)
rv |= Alt;
if (mod & NSEventModifierFlagCommand)
rv |= Windows;
if (_isLeftPressed)
rv |= LeftMouseButton;
if (_isMiddlePressed)
rv |= MiddleMouseButton;
if (_isRightPressed)
rv |= RightMouseButton;
if (_isXButton1Pressed)
rv |= XButton1MouseButton;
if (_isXButton2Pressed)
rv |= XButton2MouseButton;
return (AvnInputModifiers)rv;
}
- (BOOL)hasMarkedText
{
return _lastKeyHandled;
}
- (NSRange)markedRange
{
return NSMakeRange(NSNotFound, 0);
}
- (NSRange)selectedRange
{
return NSMakeRange(NSNotFound, 0);
}
- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange
{
}
- (void)unmarkText
{
}
- (NSArray<NSString *> *)validAttributesForMarkedText
{
return [NSArray new];
}
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange
{
return [NSAttributedString new];
}
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
{
if(!_lastKeyHandled)
{
if(_parent != nullptr)
{
_lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]);
}
}
}
- (NSUInteger)characterIndexForPoint:(NSPoint)point
{
return 0;
}
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange
{
CGRect result = { 0 };
return result;
}
- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)info
{
auto localPoint = [self convertPoint:[info draggingLocation] toView:self];
auto avnPoint = [AvnView toAvnPoint:localPoint];
auto point = [self translateLocalPoint:avnPoint];
auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]];
NSDragOperation nsop = [info draggingSourceOperationMask];
auto effects = ConvertDragDropEffects(nsop);
int reffects = (int)_parent->BaseEvents
->DragEvent(type, point, modifiers, effects,
CreateClipboard([info draggingPasteboard], nil),
GetAvnDataObjectHandleFromDraggingInfo(info));
NSDragOperation ret = static_cast<NSDragOperation>(0);
// Ensure that the managed part didn't add any new effects
reffects = (int)effects & reffects;
// OSX requires exactly one operation
if((reffects & (int)AvnDragDropEffects::Copy) != 0)
ret = NSDragOperationCopy;
else if((reffects & (int)AvnDragDropEffects::Move) != 0)
ret = NSDragOperationMove;
else if((reffects & (int)AvnDragDropEffects::Link) != 0)
ret = NSDragOperationLink;
if(ret == 0)
ret = NSDragOperationNone;
return ret;
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender];
}
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender];
}
- (void)draggingExited:(id <NSDraggingInfo>)sender
{
[self triggerAvnDragEvent: AvnDragEventType::Leave info:sender];
}
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone;
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone;
}
- (void)concludeDragOperation:(nullable id <NSDraggingInfo>)sender
{
}
- (AvnPlatformResizeReason)getResizeReason
{
return _resizeReason;
}
- (void)setResizeReason:(AvnPlatformResizeReason)reason
{
_resizeReason = reason;
}
- (AvnAccessibilityElement *) accessibilityChild
{
if (_accessibilityChild == nil)
{
auto peer = _parent->BaseEvents->GetAutomationPeer();
if (peer == nil)
return nil;
_accessibilityChild = [AvnAccessibilityElement acquire:peer];
}
return _accessibilityChild;
}
- (NSArray *)accessibilityChildren
{
auto child = [self accessibilityChild];
return NSAccessibilityUnignoredChildrenForOnlyChild(child);
}
- (id)accessibilityHitTest:(NSPoint)point
{
return [[self accessibilityChild] accessibilityHitTest:point];
}
- (id)accessibilityFocusedUIElement
{
return [[self accessibilityChild] accessibilityFocusedUIElement];
}
@end

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

@ -0,0 +1,17 @@
//
// 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
@class AvnView;
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

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

@ -0,0 +1,25 @@
//
// 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 AvnView;
class ResizeScope
{
public:
ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason);
~ResizeScope();
private:
AvnView* _Nonnull _view;
AvnPlatformResizeReason _restore;
};
#endif //AVALONIA_NATIVE_OSX_RESIZESCOPE_H

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

@ -0,0 +1,18 @@
//
// Created by Dan Walmsley on 04/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#import <AppKit/AppKit.h>
#include "ResizeScope.h"
#import "AvnView.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 "common.h"
#include "window.h" #include "window.h"
#include "INSWindowHolder.h"
class SystemDialogs : public ComSingleObject<IAvnSystemDialogs, &IID_IAvnSystemDialogs> class SystemDialogs : public ComSingleObject<IAvnSystemDialogs, &IID_IAvnSystemDialogs>
{ {

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

@ -0,0 +1,121 @@
//
// 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 AutoFitContentView;
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"
#import "AvnView.h"
#include "menu.h"
#include "automation.h"
#import "cursor.h"
#include "ResizeScope.h"
#import "AutoFitContentView.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

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

@ -0,0 +1,539 @@
//
// Created by Dan Walmsley on 04/05/2022.
// Copyright (c) 2022 Avalonia. All rights reserved.
//
#import <AppKit/AppKit.h>
#import "window.h"
#import "AutoFitContentView.h"
#import "AvnView.h"
#include "automation.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; _isHandlingSendEvent = oldHandling;
} }
} }
// This is needed for certain embedded controls
- (BOOL) isHandlingSendEvent
{
return _isHandlingSendEvent;
}
- (void)setHandlingSendEvent:(BOOL)handlingSendEvent
{
_isHandlingSendEvent = handlingSendEvent;
}
@end @end
extern void InitializeAvnApp(IAvnApplicationEvents* events) extern void InitializeAvnApp(IAvnApplicationEvents* events)

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

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

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

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

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

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

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

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

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

@ -343,7 +343,7 @@ public:
@autoreleasepool @autoreleasepool
{ {
::SetAppMenu(s_appTitle, appMenu); ::SetAppMenu(appMenu);
return S_OK; return S_OK;
} }
} }
@ -428,7 +428,3 @@ AvnPoint ConvertPointY (AvnPoint p)
return 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 NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem
IAvnActionCallback* _callback; IAvnActionCallback* _callback;
IAvnPredicateCallback* _predicate; IAvnPredicateCallback* _predicate;
bool _isSeparator;
bool _isCheckable; bool _isCheckable;
public: public:

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

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

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

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

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

@ -1,30 +1,13 @@
#ifndef window_h #ifndef window_h
#define window_h #define window_h
class WindowBaseImpl; #import "avalonia-native.h"
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination> @class AvnMenu;
-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(NSEvent* _Nonnull) lastMouseDownEvent;
-(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;
@end
@interface AutoFitContentView : NSView 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> @interface AvnWindow : NSWindow <NSWindowDelegate>
+(void) closeAll;
-(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(void) setCanBecomeKeyAndMain; -(void) setCanBecomeKeyAndMain;
-(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) pollModalSession: (NSModalSession _Nonnull) session;
@ -33,45 +16,11 @@ class WindowBaseImpl;
-(void) setEnabled: (bool) enable; -(void) setEnabled: (bool) enable;
-(void) showAppMenuOnly; -(void) showAppMenuOnly;
-(void) showWindowMenuWithAppMenu; -(void) showWindowMenuWithAppMenu;
-(void) applyMenu:(NSMenu* _Nullable)menu; -(void) applyMenu:(AvnMenu* _Nullable)menu;
-(double) getScaling;
-(double) getExtendedTitleBarHeight; -(double) getExtendedTitleBarHeight;
-(void) setIsExtended:(bool)value; -(void) setIsExtended:(bool)value;
-(bool) isDialog; -(bool) isDialog;
@end @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 */ #endif /* window_h */

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

File diff suppressed because it is too large

4
samples/ControlCatalog/App.xaml.cs

@ -20,12 +20,12 @@ namespace ControlCatalog
public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{ {
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent.xaml") Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml")
}; };
public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{ {
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default.xaml") Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml")
}; };
public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))

86
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -3,27 +3,77 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls" xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" 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"> x:Class="ControlCatalog.Pages.ColorPickerPage">
<Grid ColumnDefinitions="Auto,Auto" <UserControl.Resources>
RowDefinitions="Auto,Auto"> <pc:ThirdComponentConverter x:Key="ThirdComponent" />
<ColorSpectrum Grid.Column="0" </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" Grid.Row="0"
Color="Red" IsAlphaMaxForced="True"
Height="256" IsSaturationValueMaxForced="False"
Width="256" /> ColorComponent="{Binding Components, ElementName=ColorSpectrum2, Converter={StaticResource ThirdComponent}}"
<ColorSpectrum Grid.Column="1" 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" Grid.Row="0"
Color="Green" ColorComponent="Alpha"
Shape="Ring" ColorModel="Hsva"
Height="256" Orientation="Vertical"
Width="256" /> HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
<ColorSpectrum Grid.Column="0" <ColorPreviewer Grid.Column="0"
Grid.Row="1" Grid.ColumnSpan="3"
CornerRadius="10" Grid.Row="1"
Color="Blue" ShowAccentColors="True"
Height="256" HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
Width="256" /> </Grid>
</Grid> </Grid>
</UserControl> </UserControl>

10
samples/ControlCatalog/Pages/ComboBoxPage.xaml

@ -86,6 +86,16 @@
<sys:Exception /> <sys:Exception />
</DataValidationErrors.Error> </DataValidationErrors.Error>
</ComboBox> </ComboBox>
<ComboBox PlaceholderText="Scaled" Width="166" RenderTransformOrigin="0,0">
<ComboBox.RenderTransform>
<ScaleTransform ScaleX="1.5" ScaleY="1.5"/>
</ComboBox.RenderTransform>
<ComboBoxItem>Inline Items</ComboBoxItem>
<ComboBoxItem>Inline Item 2</ComboBoxItem>
<ComboBoxItem>Inline Item 3</ComboBoxItem>
<ComboBoxItem>Inline Item 4</ComboBoxItem>
</ComboBox>
</WrapPanel> </WrapPanel>
<CheckBox IsChecked="{Binding WrapSelection}">WrapSelection</CheckBox> <CheckBox IsChecked="{Binding WrapSelection}">WrapSelection</CheckBox>

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

@ -40,19 +40,16 @@ namespace Avalonia.Controls
control = control ?? throw new ArgumentNullException(nameof(control)); control = control ?? throw new ArgumentNullException(nameof(control));
key = key ?? throw new ArgumentNullException(nameof(key)); key = key ?? throw new ArgumentNullException(nameof(key));
IResourceHost? current = control; IResourceNode? current = control;
while (current != null) 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; value = null;

51
src/Avalonia.Base/Logging/ILogSink.cs

@ -26,57 +26,6 @@ namespace Avalonia.Logging
object? source, object? source,
string messageTemplate); string messageTemplate);
/// <summary>
/// Logs an event.
/// </summary>
/// <param name="level">The log event level.</param>
/// <param name="area">The area that the event originates.</param>
/// <param name="source">The object from which the event originates.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="propertyValue0">Message property value.</param>
void Log<T0>(
LogEventLevel level,
string area,
object? source,
string messageTemplate,
T0 propertyValue0);
/// <summary>
/// Logs an event.
/// </summary>
/// <param name="level">The log event level.</param>
/// <param name="area">The area that the event originates.</param>
/// <param name="source">The object from which the event originates.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="propertyValue0">Message property value.</param>
/// <param name="propertyValue1">Message property value.</param>
void Log<T0, T1>(
LogEventLevel level,
string area,
object? source,
string messageTemplate,
T0 propertyValue0,
T1 propertyValue1);
/// <summary>
/// Logs an event.
/// </summary>
/// <param name="level">The log event level.</param>
/// <param name="area">The area that the event originates.</param>
/// <param name="source">The object from which the event originates.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="propertyValue0">Message property value.</param>
/// <param name="propertyValue1">Message property value.</param>
/// <param name="propertyValue2">Message property value.</param>
void Log<T0, T1, T2>(
LogEventLevel level,
string area,
object? source,
string messageTemplate,
T0 propertyValue0,
T1 propertyValue1,
T2 propertyValue2);
/// <summary> /// <summary>
/// Logs a new event. /// Logs a new event.
/// </summary> /// </summary>

38
src/Avalonia.Base/Logging/TraceLogSink.cs

@ -28,31 +28,7 @@ namespace Avalonia.Logging
{ {
if (IsEnabled(level, area)) if (IsEnabled(level, area))
{ {
Trace.WriteLine(Format<object, object, object>(area, messageTemplate, source)); Trace.WriteLine(Format<object, object, object>(area, messageTemplate, source, null));
}
}
public void Log<T0>(LogEventLevel level, string area, object? source, string messageTemplate, T0 propertyValue0)
{
if (IsEnabled(level, area))
{
Trace.WriteLine(Format<T0, object, object>(area, messageTemplate, source, propertyValue0));
}
}
public void Log<T0, T1>(LogEventLevel level, string area, object? source, string messageTemplate, T0 propertyValue0, T1 propertyValue1)
{
if (IsEnabled(level, area))
{
Trace.WriteLine(Format<T0, T1, object>(area, messageTemplate, source, propertyValue0, propertyValue1));
}
}
public void Log<T0, T1, T2>(LogEventLevel level, string area, object? source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2)
{
if (IsEnabled(level, area))
{
Trace.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1, propertyValue2));
} }
} }
@ -68,9 +44,7 @@ namespace Avalonia.Logging
string area, string area,
string template, string template,
object? source, object? source,
T0? v0 = default, object?[]? values)
T1? v1 = default,
T2? v2 = default)
{ {
var result = new StringBuilder(template.Length); var result = new StringBuilder(template.Length);
var r = new CharacterReader(template.AsSpan()); var r = new CharacterReader(template.AsSpan());
@ -93,13 +67,7 @@ namespace Avalonia.Logging
if (r.Peek != '{') if (r.Peek != '{')
{ {
result.Append('\''); result.Append('\'');
result.Append(i++ switch result.Append(values?[i++]);
{
0 => v0,
1 => v1,
2 => v2,
_ => null
});
result.Append('\''); result.Append('\'');
r.TakeUntil('}'); r.TakeUntil('}');
r.Take(); r.Take();

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

@ -528,7 +528,7 @@ namespace Avalonia.Media.TextFormatting
/// Creates an empty text line. /// Creates an empty text line.
/// </summary> /// </summary>
/// <returns>The empty text line.</returns> /// <returns>The empty text line.</returns>
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, TextParagraphProperties paragraphProperties) public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties)
{ {
var flowDirection = paragraphProperties.FlowDirection; var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties; var properties = paragraphProperties.DefaultTextRunProperties;
@ -542,7 +542,7 @@ namespace Avalonia.Media.TextFormatting
var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) }; var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
return new TextLineImpl(textRuns, firstTextSourceIndex, 0, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine(); return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine();
} }
/// <summary> /// <summary>
@ -561,7 +561,7 @@ namespace Avalonia.Media.TextFormatting
{ {
if(textRuns.Count == 0) if(textRuns.Count == 0)
{ {
return CreateEmptyTextLine(firstTextSourceIndex, paragraphProperties); return CreateEmptyTextLine(firstTextSourceIndex,paragraphWidth, paragraphProperties);
} }
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))

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

@ -410,7 +410,7 @@ namespace Avalonia.Media.TextFormatting
{ {
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties); var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
Bounds = new Rect(0,0,0, textLine.Height); Bounds = new Rect(0,0,0, textLine.Height);
@ -434,7 +434,7 @@ namespace Avalonia.Media.TextFormatting
{ {
if(previousLine != null && previousLine.NewLineLength > 0) if(previousLine != null && previousLine.NewLineLength > 0)
{ {
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, _paragraphProperties); var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties);
textLines.Add(emptyTextLine); textLines.Add(emptyTextLine);
@ -483,7 +483,7 @@ namespace Avalonia.Media.TextFormatting
//Make sure the TextLayout always contains at least on empty line //Make sure the TextLayout always contains at least on empty line
if(textLines.Count == 0) if(textLines.Count == 0)
{ {
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties); var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
textLines.Add(textLine); textLines.Add(textLine);

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

@ -252,7 +252,7 @@ namespace Avalonia.Media.TextFormatting
//Look at the left and right edge of the current run //Look at the left and right edge of the current run
if (currentRun.IsLeftToRight) if (currentRun.IsLeftToRight)
{ {
if (lastRun == null || lastRun.IsLeftToRight) if (_flowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight))
{ {
if (characterIndex <= textRun.Text.Start) if (characterIndex <= textRun.Text.Start)
{ {
@ -418,11 +418,6 @@ namespace Avalonia.Media.TextFormatting
continue; continue;
} }
if(currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex)
{
continue;
}
TextRun? nextRun = null; TextRun? nextRun = null;
if (index + 1 < TextRuns.Count) if (index + 1 < TextRuns.Count)
@ -460,7 +455,7 @@ namespace Avalonia.Media.TextFormatting
} }
default: default:
{ {
goto noop; goto noop;
} }
} }
@ -541,6 +536,11 @@ namespace Avalonia.Media.TextFormatting
endX += currentRun.Size.Width; endX += currentRun.Size.Width;
} }
if (currentPosition < firstTextSourceCharacterIndex)
{
startX += currentRun.Size.Width;
}
currentPosition += currentRun.TextSourceLength; currentPosition += currentRun.TextSourceLength;
break; break;
@ -554,24 +554,29 @@ namespace Avalonia.Media.TextFormatting
var width = endX - startX; var width = endX - startX;
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) if (!MathUtilities.IsZero(width))
{ {
currentRect = currentRect.WithWidth(currentRect.Width + width); if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
currentRect = currentRect.WithWidth(currentRect.Width + width);
var textBounds = new TextBounds(currentRect, currentDirection); var textBounds = new TextBounds(currentRect, currentDirection);
result[result.Count - 1] = textBounds; result[result.Count - 1] = textBounds;
} }
else else
{ {
currentRect = new Rect(startX, 0, width, Height);
result.Add(new TextBounds(currentRect, currentDirection)); currentRect = new Rect(startX, 0, width, Height);
result.Add(new TextBounds(currentRect, currentDirection));
}
} }
if (currentDirection == FlowDirection.LeftToRight) if (currentDirection == FlowDirection.LeftToRight)
{ {
if (currentPosition >= firstTextSourceCharacterIndex + textLength) if (currentPosition > firstTextSourceCharacterIndex + textLength)
{ {
break; break;
} }
@ -595,10 +600,10 @@ namespace Avalonia.Media.TextFormatting
public TextLineImpl FinalizeLine() public TextLineImpl FinalizeLine()
{ {
BidiReorder();
_textLineMetrics = CreateLineMetrics(); _textLineMetrics = CreateLineMetrics();
BidiReorder();
return this; return this;
} }
@ -1026,7 +1031,7 @@ namespace Avalonia.Media.TextFormatting
var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface; var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface;
var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight; var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
var width = 0d; var width = 0d;
var widthIncludingWhitespace = 0d; var widthIncludingWhitespace = 0d;
var trailingWhitespaceLength = 0; var trailingWhitespaceLength = 0;
@ -1036,8 +1041,8 @@ namespace Avalonia.Media.TextFormatting
var lineGap = glyphTypeface.LineGap * scale; var lineGap = glyphTypeface.LineGap * scale;
var height = descent - ascent + lineGap; var height = descent - ascent + lineGap;
var lineHeight = _paragraphProperties.LineHeight; var lineHeight = _paragraphProperties.LineHeight;
for (var index = 0; index < _textRuns.Count; index++) for (var index = 0; index < _textRuns.Count; index++)
{ {
@ -1073,41 +1078,11 @@ namespace Avalonia.Media.TextFormatting
} }
} }
switch (_paragraphProperties.FlowDirection) if (index == _textRuns.Count - 1)
{ {
case FlowDirection.LeftToRight: width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
{ trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
if (index == _textRuns.Count - 1) newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
{
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;
}
} }
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace; widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
@ -1166,10 +1141,10 @@ namespace Avalonia.Media.TextFormatting
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
{ {
if(lineHeight > height) if (lineHeight > height)
{ {
height = lineHeight; height = lineHeight;
} }
} }
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start, return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,

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

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

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

@ -160,7 +160,7 @@ namespace Avalonia.Styling
for (var i = Count - 1; i >= 0; --i) 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; return true;
} }

2
src/Avalonia.Base/Utilities/WeakHashList.cs

@ -118,7 +118,7 @@ internal class WeakHashList<T> where T : class
{ {
if (_arr != null) if (_arr != null)
{ {
for (var c = 0; c < _arr.Length; c++) for (var c = 0; c < _arrCount; c++)
{ {
if (_arr[c]?.TryGetTarget(out var target) == true && target == item) if (_arr[c]?.TryGetTarget(out var target) == true && target == item)
{ {

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

414
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs

@ -1,414 +0,0 @@
// 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 System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Avalonia.Controls.Primitives
{
internal static class ColorHelpers
{
public const int CheckerSize = 4;
public static bool ToDisplayNameExists
{
get => false;
}
public static string ToDisplayName(Color color)
{
return string.Empty;
}
public static Hsv IncrementColorComponent(
Hsv originalHsv,
HsvComponent component,
IncrementDirection direction,
IncrementAmount amount,
bool shouldWrap,
double minBound,
double maxBound)
{
Hsv newHsv = originalHsv;
if (amount == IncrementAmount.Small || !ToDisplayNameExists)
{
// In order to avoid working with small values that can incur rounding issues,
// we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1.
newHsv.S *= 100;
newHsv.V *= 100;
// Note: *valueToIncrement replaced with ref local variable for C#, must be initialized
ref double valueToIncrement = ref newHsv.H;
double incrementAmount = 0.0;
// If we're adding a small increment, then we'll just add or subtract 1.
// If we're adding a large increment, then we want to snap to the next
// or previous major value - for hue, this is every increment of 30;
// for saturation and value, this is every increment of 10.
switch (component)
{
case HsvComponent.Hue:
valueToIncrement = ref newHsv.H;
incrementAmount = amount == IncrementAmount.Small ? 1 : 30;
break;
case HsvComponent.Saturation:
valueToIncrement = ref newHsv.S;
incrementAmount = amount == IncrementAmount.Small ? 1 : 10;
break;
case HsvComponent.Value:
valueToIncrement = ref newHsv.V;
incrementAmount = amount == IncrementAmount.Small ? 1 : 10;
break;
default:
throw new InvalidOperationException("Invalid HsvComponent.");
}
double previousValue = valueToIncrement;
valueToIncrement += (direction == IncrementDirection.Lower ? -incrementAmount : incrementAmount);
// If the value has reached outside the bounds, we were previous at the boundary, and we should wrap,
// then we'll place the selection on the other side of the spectrum.
// Otherwise, we'll place it on the boundary that was exceeded.
if (valueToIncrement < minBound)
{
valueToIncrement = (shouldWrap && previousValue == minBound) ? maxBound : minBound;
}
if (valueToIncrement > maxBound)
{
valueToIncrement = (shouldWrap && previousValue == maxBound) ? minBound : maxBound;
}
// We multiplied saturation and value by 100 previously, so now we want to put them back in the 0-1 range.
newHsv.S /= 100;
newHsv.V /= 100;
}
else
{
// While working with named colors, we're going to need to be working in actual HSV units,
// so we'll divide the min bound and max bound by 100 in the case of saturation or value,
// since we'll have received units between 0-100 and we need them within 0-1.
if (component == HsvComponent.Saturation ||
component == HsvComponent.Value)
{
minBound /= 100;
maxBound /= 100;
}
newHsv = FindNextNamedColor(originalHsv, component, direction, shouldWrap, minBound, maxBound);
}
return newHsv;
}
public static Hsv FindNextNamedColor(
Hsv originalHsv,
HsvComponent component,
IncrementDirection direction,
bool shouldWrap,
double minBound,
double maxBound)
{
// There's no easy way to directly get the next named color, so what we'll do
// is just iterate in the direction that we want to find it until we find a color
// in that direction that has a color name different than our current color name.
// Once we find a new color name, then we'll iterate across that color name until
// we find its bounds on the other side, and then select the color that is exactly
// in the middle of that color's bounds.
Hsv newHsv = originalHsv;
string originalColorName = ColorHelpers.ToDisplayName(originalHsv.ToRgb().ToColor());
string newColorName = originalColorName;
// Note: *newValue replaced with ref local variable for C#, must be initialized
double originalValue = 0.0;
ref double newValue = ref newHsv.H;
double incrementAmount = 0.0;
switch (component)
{
case HsvComponent.Hue:
originalValue = originalHsv.H;
newValue = ref newHsv.H;
incrementAmount = 1;
break;
case HsvComponent.Saturation:
originalValue = originalHsv.S;
newValue = ref newHsv.S;
incrementAmount = 0.01;
break;
case HsvComponent.Value:
originalValue = originalHsv.V;
newValue = ref newHsv.V;
incrementAmount = 0.01;
break;
default:
throw new InvalidOperationException("Invalid HsvComponent.");
}
bool shouldFindMidPoint = true;
while (newColorName == originalColorName)
{
double previousValue = newValue;
newValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount;
bool justWrapped = false;
// If we've hit a boundary, then either we should wrap or we shouldn't.
// If we should, then we'll perform that wrapping if we were previously up against
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
if (newValue > maxBound)
{
if (shouldWrap)
{
newValue = minBound;
justWrapped = true;
}
else
{
newValue = maxBound;
shouldFindMidPoint = false;
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
break;
}
}
else if (newValue < minBound)
{
if (shouldWrap)
{
newValue = maxBound;
justWrapped = true;
}
else
{
newValue = minBound;
shouldFindMidPoint = false;
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
break;
}
}
if (!justWrapped &&
previousValue != originalValue &&
Math.Sign(newValue - originalValue) != Math.Sign(previousValue - originalValue))
{
// If we've wrapped all the way back to the start and have failed to find a new color name,
// then we'll just quit - there isn't a new color name that we're going to find.
shouldFindMidPoint = false;
break;
}
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
}
if (shouldFindMidPoint)
{
Hsv startHsv = newHsv;
Hsv currentHsv = startHsv;
double startEndOffset = 0;
string currentColorName = newColorName;
// Note: *startValue/*currentValue replaced with ref local variables for C#, must be initialized
ref double startValue = ref startHsv.H;
ref double currentValue = ref currentHsv.H;
double wrapIncrement = 0;
switch (component)
{
case HsvComponent.Hue:
startValue = ref startHsv.H;
currentValue = ref currentHsv.H;
wrapIncrement = 360.0;
break;
case HsvComponent.Saturation:
startValue = ref startHsv.S;
currentValue = ref currentHsv.S;
wrapIncrement = 1.0;
break;
case HsvComponent.Value:
startValue = ref startHsv.V;
currentValue = ref currentHsv.V;
wrapIncrement = 1.0;
break;
default:
throw new InvalidOperationException("Invalid HsvComponent.");
}
while (newColorName == currentColorName)
{
currentValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount;
// If we've hit a boundary, then either we should wrap or we shouldn't.
// If we should, then we'll perform that wrapping if we were previously up against
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
if (currentValue > maxBound)
{
if (shouldWrap)
{
currentValue = minBound;
startEndOffset = maxBound - minBound;
}
else
{
currentValue = maxBound;
break;
}
}
else if (currentValue < minBound)
{
if (shouldWrap)
{
currentValue = maxBound;
startEndOffset = minBound - maxBound;
}
else
{
currentValue = minBound;
break;
}
}
currentColorName = ColorHelpers.ToDisplayName(currentHsv.ToRgb().ToColor());
}
newValue = (startValue + currentValue + startEndOffset) / 2;
// Dividing by 2 may have gotten us halfway through a single step, so we'll
// remove that half-step if it exists.
double leftoverValue = Math.Abs(newValue);
while (leftoverValue > incrementAmount)
{
leftoverValue -= incrementAmount;
}
newValue -= leftoverValue;
while (newValue < minBound)
{
newValue += wrapIncrement;
}
while (newValue > maxBound)
{
newValue -= wrapIncrement;
}
}
return newHsv;
}
public static double IncrementAlphaComponent(
double originalAlpha,
IncrementDirection direction,
IncrementAmount amount,
bool shouldWrap,
double minBound,
double maxBound)
{
// In order to avoid working with small values that can incur rounding issues,
// we'll multiple alpha by 100 to put it in the range of 0-100 instead of 0-1.
originalAlpha *= 100;
const double smallIncrementAmount = 1;
const double largeIncrementAmount = 10;
if (amount == IncrementAmount.Small)
{
originalAlpha += (direction == IncrementDirection.Lower ? -1 : 1) * smallIncrementAmount;
}
else
{
if (direction == IncrementDirection.Lower)
{
originalAlpha = Math.Ceiling((originalAlpha - largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount;
}
else
{
originalAlpha = Math.Floor((originalAlpha + largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount;
}
}
// If the value has reached outside the bounds and we should wrap, then we'll place the selection
// on the other side of the spectrum. Otherwise, we'll place it on the boundary that was exceeded.
if (originalAlpha < minBound)
{
originalAlpha = shouldWrap ? maxBound : minBound;
}
if (originalAlpha > maxBound)
{
originalAlpha = shouldWrap ? minBound : maxBound;
}
// We multiplied alpha by 100 previously, so now we want to put it back in the 0-1 range.
return originalAlpha / 100;
}
public static WriteableBitmap CreateBitmapFromPixelData(
int pixelWidth,
int pixelHeight,
List<byte> bgraPixelData)
{
Vector dpi = new Vector(96, 96); // Standard may need to change on some devices
WriteableBitmap bitmap = new WriteableBitmap(
new PixelSize(pixelWidth, pixelHeight),
dpi,
PixelFormat.Bgra8888,
AlphaFormat.Premul);
// Warning: This is highly questionable
using (var frameBuffer = bitmap.Lock())
{
Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count);
}
return bitmap;
}
/// <summary>
/// Gets the relative (perceptual) luminance/brightness of the given color.
/// 1 is closer to white while 0 is closer to black.
/// </summary>
/// <param name="color">The color to calculate relative luminance for.</param>
/// <returns>The relative (perceptual) luminance/brightness of the given color.</returns>
public static double GetRelativeLuminance(Color color)
{
// The equation for relative luminance is given by
//
// L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg
//
// where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise }
//
// If L is closer to 1, then the color is closer to white; if it is closer to 0,
// then the color is closer to black. This is based on the fact that the human
// eye perceives green to be much brighter than red, which in turn is perceived to be
// brighter than blue.
double rg = color.R <= 10 ? color.R / 3294.0 : Math.Pow(color.R / 269.0 + 0.0513, 2.4);
double gg = color.G <= 10 ? color.G / 3294.0 : Math.Pow(color.G / 269.0 + 0.0513, 2.4);
double bg = color.B <= 10 ? color.B / 3294.0 : Math.Pow(color.B / 269.0 + 0.0513, 2.4);
return (0.2126 * rg + 0.7152 * gg + 0.0722 * bg);
}
}
}

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

@ -3,6 +3,7 @@
// //
// Licensed to The Avalonia Project under the MIT License. // Licensed to The Avalonia Project under the MIT License.
using Avalonia.Data;
using Avalonia.Media; using Avalonia.Media;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
@ -10,6 +11,88 @@ namespace Avalonia.Controls.Primitives
/// <inheritdoc/> /// <inheritdoc/>
public partial class ColorSpectrum 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> /// <summary>
/// Gets or sets the currently selected color in the RGB color model. /// Gets or sets the currently selected color in the RGB color model.
/// </summary> /// </summary>
@ -23,14 +106,6 @@ namespace Avalonia.Controls.Primitives
set => SetValue(ColorProperty, value); set => SetValue(ColorProperty, value);
} }
/// <summary>
/// Defines the <see cref="Color"/> property.
/// </summary>
public static readonly StyledProperty<Color> ColorProperty =
AvaloniaProperty.Register<ColorSpectrum, Color>(
nameof(Color),
Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF));
/// <summary> /// <summary>
/// Gets or sets the two HSV color components displayed by the spectrum. /// Gets or sets the two HSV color components displayed by the spectrum.
/// </summary> /// </summary>
@ -43,14 +118,6 @@ namespace Avalonia.Controls.Primitives
set => SetValue(ComponentsProperty, value); set => SetValue(ComponentsProperty, value);
} }
/// <summary>
/// Defines the <see cref="Components"/> property.
/// </summary>
public static readonly StyledProperty<ColorSpectrumComponents> ComponentsProperty =
AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumComponents>(
nameof(Components),
ColorSpectrumComponents.HueSaturation);
/// <summary> /// <summary>
/// Gets or sets the currently selected color in the HSV color model. /// Gets or sets the currently selected color in the HSV color model.
/// </summary> /// </summary>
@ -65,14 +132,6 @@ namespace Avalonia.Controls.Primitives
set => SetValue(HsvColorProperty, value); set => SetValue(HsvColorProperty, value);
} }
/// <summary>
/// Defines the <see cref="HsvColor"/> property.
/// </summary>
public static readonly StyledProperty<HsvColor> HsvColorProperty =
AvaloniaProperty.Register<ColorSpectrum, HsvColor>(
nameof(HsvColor),
new HsvColor(1, 0, 0, 1));
/// <summary> /// <summary>
/// Gets or sets the maximum value of the Hue component in the range from 0..359. /// 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"/>. /// This property must be greater than <see cref="MinHue"/>.
@ -86,12 +145,6 @@ namespace Avalonia.Controls.Primitives
set => SetValue(MaxHueProperty, value); set => SetValue(MaxHueProperty, value);
} }
/// <summary>
/// Defines the <see cref="MaxHue"/> property.
/// </summary>
public static readonly StyledProperty<int> MaxHueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MaxHue), 359);
/// <summary> /// <summary>
/// Gets or sets the maximum value of the Saturation component in the range from 0..100. /// 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"/>. /// This property must be greater than <see cref="MinSaturation"/>.
@ -105,12 +158,6 @@ namespace Avalonia.Controls.Primitives
set => SetValue(MaxSaturationProperty, value); set => SetValue(MaxSaturationProperty, value);
} }
/// <summary>
/// Defines the <see cref="MaxSaturation"/> property.
/// </summary>
public static readonly StyledProperty<int> MaxSaturationProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MaxSaturation), 100);
/// <summary> /// <summary>
/// Gets or sets the maximum value of the Value component in the range from 0..100. /// 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"/>. /// This property must be greater than <see cref="MinValue"/>.
@ -124,12 +171,6 @@ namespace Avalonia.Controls.Primitives
set => SetValue(MaxValueProperty, value); set => SetValue(MaxValueProperty, value);
} }
/// <summary>
/// Defines the <see cref="MaxValue"/> property.
/// </summary>
public static readonly StyledProperty<int> MaxValueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MaxValue), 100);
/// <summary> /// <summary>
/// Gets or sets the minimum value of the Hue component in the range from 0..359. /// 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"/>. /// This property must be less than <see cref="MaxHue"/>.
@ -143,12 +184,6 @@ namespace Avalonia.Controls.Primitives
set => SetValue(MinHueProperty, value); set => SetValue(MinHueProperty, value);
} }
/// <summary>
/// Defines the <see cref="MinHue"/> property.
/// </summary>
public static readonly StyledProperty<int> MinHueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MinHue), 0);
/// <summary> /// <summary>
/// Gets or sets the minimum value of the Saturation component in the range from 0..100. /// 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"/>. /// This property must be less than <see cref="MaxSaturation"/>.
@ -162,12 +197,6 @@ namespace Avalonia.Controls.Primitives
set => SetValue(MinSaturationProperty, value); set => SetValue(MinSaturationProperty, value);
} }
/// <summary>
/// Defines the <see cref="MinSaturation"/> property.
/// </summary>
public static readonly StyledProperty<int> MinSaturationProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MinSaturation), 0);
/// <summary> /// <summary>
/// Gets or sets the minimum value of the Value component in the range from 0..100. /// 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"/>. /// This property must be less than <see cref="MaxValue"/>.
@ -181,12 +210,6 @@ namespace Avalonia.Controls.Primitives
set => SetValue(MinValueProperty, value); set => SetValue(MinValueProperty, value);
} }
/// <summary>
/// Defines the <see cref="MinValue"/> property.
/// </summary>
public static readonly StyledProperty<int> MinValueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MinValue), 0);
/// <summary> /// <summary>
/// Gets or sets the displayed shape of the spectrum. /// Gets or sets the displayed shape of the spectrum.
/// </summary> /// </summary>
@ -195,13 +218,5 @@ namespace Avalonia.Controls.Primitives
get => GetValue(ShapeProperty); get => GetValue(ShapeProperty);
set => SetValue(ShapeProperty, value); set => SetValue(ShapeProperty, value);
} }
/// <summary>
/// Defines the <see cref="Shape"/> property.
/// </summary>
public static readonly StyledProperty<ColorSpectrumShape> ShapeProperty =
AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumShape>(
nameof(Shape),
ColorSpectrumShape.Box);
} }
} }

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

@ -10,6 +10,7 @@ using Avalonia.Controls.Metadata;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Threading; using Avalonia.Threading;
@ -20,7 +21,6 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// A two dimensional spectrum for color selection. /// A two dimensional spectrum for color selection.
/// </summary> /// </summary>
[TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))]
[TemplatePart("PART_InputTarget", typeof(Canvas))] [TemplatePart("PART_InputTarget", typeof(Canvas))]
[TemplatePart("PART_LayoutRoot", typeof(Panel))] [TemplatePart("PART_LayoutRoot", typeof(Panel))]
[TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))] [TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))]
@ -29,10 +29,11 @@ namespace Avalonia.Controls.Primitives
[TemplatePart("PART_SpectrumRectangle", typeof(Rectangle))] [TemplatePart("PART_SpectrumRectangle", typeof(Rectangle))]
[TemplatePart("PART_SpectrumOverlayEllipse", typeof(Ellipse))] [TemplatePart("PART_SpectrumOverlayEllipse", typeof(Ellipse))]
[TemplatePart("PART_SpectrumOverlayRectangle", typeof(Rectangle))] [TemplatePart("PART_SpectrumOverlayRectangle", typeof(Rectangle))]
[PseudoClasses(pcPressed, pcLargeSelector, pcLightSelector)] [PseudoClasses(pcPressed, pcLargeSelector, pcDarkSelector, pcLightSelector)]
public partial class ColorSpectrum : TemplatedControl public partial class ColorSpectrum : TemplatedControl
{ {
protected const string pcPressed = ":pressed"; protected const string pcPressed = ":pressed";
protected const string pcDarkSelector = ":dark-selector";
protected const string pcLargeSelector = ":large-selector"; protected const string pcLargeSelector = ":large-selector";
protected const string pcLightSelector = ":light-selector"; protected const string pcLightSelector = ":light-selector";
@ -60,7 +61,6 @@ namespace Avalonia.Controls.Primitives
private Ellipse? _spectrumOverlayEllipse; private Ellipse? _spectrumOverlayEllipse;
private Canvas? _inputTarget; private Canvas? _inputTarget;
private Panel? _selectionEllipsePanel; private Panel? _selectionEllipsePanel;
private ToolTip? _colorNameToolTip;
// Put the spectrum images in a bitmap, which is then given to an ImageBrush. // Put the spectrum images in a bitmap, which is then given to an ImageBrush.
private WriteableBitmap? _hueRedBitmap; private WriteableBitmap? _hueRedBitmap;
@ -117,7 +117,6 @@ namespace Avalonia.Controls.Primitives
UnregisterEvents(); // Failsafe UnregisterEvents(); // Failsafe
_colorNameToolTip = e.NameScope.Find<ToolTip>("PART_ColorNameToolTip");
_inputTarget = e.NameScope.Find<Canvas>("PART_InputTarget"); _inputTarget = e.NameScope.Find<Canvas>("PART_InputTarget");
_layoutRoot = e.NameScope.Find<Panel>("PART_LayoutRoot"); _layoutRoot = e.NameScope.Find<Panel>("PART_LayoutRoot");
_selectionEllipsePanel = e.NameScope.Find<Panel>("PART_SelectionEllipsePanel"); _selectionEllipsePanel = e.NameScope.Find<Panel>("PART_SelectionEllipsePanel");
@ -152,10 +151,10 @@ namespace Avalonia.Controls.Primitives
}); });
} }
if (ColorHelpers.ToDisplayNameExists && if (_selectionEllipsePanel != null &&
_colorNameToolTip != null) ColorHelper.ToDisplayNameExists)
{ {
_colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); ToolTip.SetTip(_selectionEllipsePanel, ColorHelper.ToDisplayName(Color));
} }
// If we haven't yet created our bitmaps, do so now. // If we haven't yet created our bitmaps, do so now.
@ -320,7 +319,7 @@ namespace Avalonia.Controls.Primitives
IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small; IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small;
HsvColor hsvColor = HsvColor; HsvColor hsvColor = HsvColor;
UpdateColor(ColorHelpers.IncrementColorComponent( UpdateColor(ColorPickerHelpers.IncrementColorComponent(
new Hsv(hsvColor), new Hsv(hsvColor),
incrementComponent, incrementComponent,
direction, direction,
@ -330,34 +329,51 @@ namespace Avalonia.Controls.Primitives
maxBound)); maxBound));
e.Handled = true; e.Handled = true;
return;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e) protected override void OnGotFocus(GotFocusEventArgs e)
{ {
// We only want to bother with the color name tool tip if we can provide color names. // We only want to bother with the color name tool tip if we can provide color names.
if (_colorNameToolTip != null && if (_selectionEllipsePanel != null &&
ColorHelpers.ToDisplayNameExists) ColorHelper.ToDisplayNameExists)
{ {
ToolTip.SetIsOpen(_colorNameToolTip, true); ToolTip.SetIsOpen(_selectionEllipsePanel, true);
} }
UpdatePseudoClasses(); UpdatePseudoClasses();
base.OnGotFocus(e);
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnLostFocus(RoutedEventArgs e) protected override void OnLostFocus(RoutedEventArgs e)
{ {
// We only want to bother with the color name tool tip if we can provide color names. // We only want to bother with the color name tool tip if we can provide color names.
if (_colorNameToolTip != null && if (_selectionEllipsePanel != null &&
ColorHelpers.ToDisplayNameExists) ColorHelper.ToDisplayNameExists)
{ {
ToolTip.SetIsOpen(_colorNameToolTip, false); ToolTip.SetIsOpen(_selectionEllipsePanel, false);
} }
UpdatePseudoClasses(); UpdatePseudoClasses();
base.OnLostFocus(e);
}
/// <inheritdoc/>
protected override void OnPointerLeave(PointerEventArgs e)
{
// We only want to bother with the color name tool tip if we can provide color names.
if (_selectionEllipsePanel != null &&
ColorHelper.ToDisplayNameExists)
{
ToolTip.SetIsOpen(_selectionEllipsePanel, false);
}
UpdatePseudoClasses();
base.OnPointerLeave(e);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -516,12 +532,10 @@ namespace Avalonia.Controls.Primitives
var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor); var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor);
ColorChanged?.Invoke(this, colorChangedEventArgs); ColorChanged?.Invoke(this, colorChangedEventArgs);
if (ColorHelpers.ToDisplayNameExists) if (_selectionEllipsePanel != null &&
ColorHelper.ToDisplayNameExists)
{ {
if (_colorNameToolTip != null) ToolTip.SetTip(_selectionEllipsePanel, ColorHelper.ToDisplayName(Color));
{
_colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor);
}
} }
} }
} }
@ -543,7 +557,16 @@ namespace Avalonia.Controls.Primitives
PseudoClasses.Set(pcLargeSelector, false); PseudoClasses.Set(pcLargeSelector, false);
} }
PseudoClasses.Set(pcLightSelector, SelectionEllipseShouldBeLight()); if (SelectionEllipseShouldBeLight())
{
PseudoClasses.Set(pcDarkSelector, false);
PseudoClasses.Set(pcLightSelector, true);
}
else
{
PseudoClasses.Set(pcDarkSelector, true);
PseudoClasses.Set(pcLightSelector, false);
}
} }
private void UpdateColor(Hsv newHsv) private void UpdateColor(Hsv newHsv)
@ -575,8 +598,10 @@ namespace Avalonia.Controls.Primitives
return; return;
} }
double xPosition = point.Position.X; // Remember the bitmap size follows physical device pixels
double yPosition = point.Position.Y; var scale = LayoutHelper.GetLayoutScale(this);
double xPosition = point.Position.X * scale;
double yPosition = point.Position.Y * scale;
double radius = Math.Min(_imageWidthFromLastBitmapCreation, _imageHeightFromLastBitmapCreation) / 2; double radius = Math.Min(_imageWidthFromLastBitmapCreation, _imageHeightFromLastBitmapCreation) / 2;
double distanceFromRadius = Math.Sqrt(Math.Pow(xPosition - radius, 2) + Math.Pow(yPosition - radius, 2)); double distanceFromRadius = Math.Sqrt(Math.Pow(xPosition - radius, 2) + Math.Pow(yPosition - radius, 2));
@ -807,19 +832,17 @@ namespace Avalonia.Controls.Primitives
yPosition = (Math.Sin((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius; yPosition = (Math.Sin((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius;
} }
Canvas.SetLeft(_selectionEllipsePanel, xPosition - (_selectionEllipsePanel.Width / 2)); // Remember the bitmap size follows physical device pixels
Canvas.SetTop(_selectionEllipsePanel, yPosition - (_selectionEllipsePanel.Height / 2)); var scale = LayoutHelper.GetLayoutScale(this);
Canvas.SetLeft(_selectionEllipsePanel, (xPosition / scale) - (_selectionEllipsePanel.Width / 2));
Canvas.SetTop(_selectionEllipsePanel, (yPosition / scale) - (_selectionEllipsePanel.Height / 2));
// We only want to bother with the color name tool tip if we can provide color names. // We only want to bother with the color name tool tip if we can provide color names.
if (ColorHelpers.ToDisplayNameExists) if (IsFocused &&
_selectionEllipsePanel != null &&
ColorHelper.ToDisplayNameExists)
{ {
if (_colorNameToolTip != null) ToolTip.SetIsOpen(_selectionEllipsePanel, true);
{
// ToolTip doesn't currently provide any way to re-run its placement logic if its placement target moves,
// so toggling IsEnabled induces it to do that without incurring any visual glitches.
_colorNameToolTip.IsEnabled = false;
_colorNameToolTip.IsEnabled = true;
}
} }
UpdatePseudoClasses(); UpdatePseudoClasses();
@ -961,7 +984,14 @@ namespace Avalonia.Controls.Primitives
List<byte> bgraMaxPixelData = new List<byte>(); List<byte> bgraMaxPixelData = new List<byte>();
List<Hsv> newHsvValues = new List<Hsv>(); List<Hsv> newHsvValues = new List<Hsv>();
var pixelCount = (int)(Math.Round(minDimension) * Math.Round(minDimension)); // 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);
int pixelDimension = (int)Math.Round(minDimension * scale);
var pixelCount = pixelDimension * pixelDimension;
var pixelDataSize = pixelCount * 4; var pixelDataSize = pixelCount * 4;
bgraMinPixelData.Capacity = pixelDataSize; bgraMinPixelData.Capacity = pixelDataSize;
@ -978,8 +1008,6 @@ namespace Avalonia.Controls.Primitives
bgraMaxPixelData.Capacity = pixelDataSize; bgraMaxPixelData.Capacity = pixelDataSize;
newHsvValues.Capacity = pixelCount; newHsvValues.Capacity = pixelCount;
int minDimensionInt = (int)Math.Round(minDimension);
await Task.Run(() => await Task.Run(() =>
{ {
// As the user perceives it, every time the third dimension not represented in the ColorSpectrum changes, // As the user perceives it, every time the third dimension not represented in the ColorSpectrum changes,
@ -998,12 +1026,12 @@ namespace Avalonia.Controls.Primitives
// but the running time savings after that are *huge* when we can just set an opacity instead of generating a brand new bitmap. // but the running time savings after that are *huge* when we can just set an opacity instead of generating a brand new bitmap.
if (shape == ColorSpectrumShape.Box) if (shape == ColorSpectrumShape.Box)
{ {
for (int x = minDimensionInt - 1; x >= 0; --x) for (int x = pixelDimension - 1; x >= 0; --x)
{ {
for (int y = minDimensionInt - 1; y >= 0; --y) for (int y = pixelDimension - 1; y >= 0; --y)
{ {
FillPixelForBox( FillPixelForBox(
x, y, hsv, minDimensionInt, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, x, y, hsv, pixelDimension, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue,
bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData,
newHsvValues); newHsvValues);
} }
@ -1011,12 +1039,12 @@ namespace Avalonia.Controls.Primitives
} }
else else
{ {
for (int y = 0; y < minDimensionInt; ++y) for (int y = 0; y < pixelDimension; ++y)
{ {
for (int x = 0; x < minDimensionInt; ++x) for (int x = 0; x < pixelDimension; ++x)
{ {
FillPixelForRing( FillPixelForRing(
x, y, minDimensionInt / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, x, y, pixelDimension / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue,
bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData,
newHsvValues); newHsvValues);
} }
@ -1026,13 +1054,13 @@ namespace Avalonia.Controls.Primitives
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
int pixelWidth = (int)Math.Round(minDimension); int pixelWidth = pixelDimension;
int pixelHeight = (int)Math.Round(minDimension); int pixelHeight = pixelDimension;
ColorSpectrumComponents components2 = Components; ColorSpectrumComponents components2 = Components;
WriteableBitmap minBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMinPixelData); WriteableBitmap minBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMinPixelData, pixelWidth, pixelHeight);
WriteableBitmap maxBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMaxPixelData); WriteableBitmap maxBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMaxPixelData, pixelWidth, pixelHeight);
switch (components2) switch (components2)
{ {
@ -1048,18 +1076,18 @@ namespace Avalonia.Controls.Primitives
case ColorSpectrumComponents.ValueSaturation: case ColorSpectrumComponents.ValueSaturation:
case ColorSpectrumComponents.SaturationValue: case ColorSpectrumComponents.SaturationValue:
_hueRedBitmap = minBitmap; _hueRedBitmap = minBitmap;
_hueYellowBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle1PixelData); _hueYellowBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle1PixelData, pixelWidth, pixelHeight);
_hueGreenBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle2PixelData); _hueGreenBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle2PixelData, pixelWidth, pixelHeight);
_hueCyanBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle3PixelData); _hueCyanBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle3PixelData, pixelWidth, pixelHeight);
_hueBlueBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle4PixelData); _hueBlueBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle4PixelData, pixelWidth, pixelHeight);
_huePurpleBitmap = maxBitmap; _huePurpleBitmap = maxBitmap;
break; break;
} }
_shapeFromLastBitmapCreation = Shape; _shapeFromLastBitmapCreation = Shape;
_componentsFromLastBitmapCreation = Components; _componentsFromLastBitmapCreation = Components;
_imageWidthFromLastBitmapCreation = minDimension; _imageWidthFromLastBitmapCreation = pixelDimension;
_imageHeightFromLastBitmapCreation = minDimension; _imageHeightFromLastBitmapCreation = pixelDimension;
_minHueFromLastBitmapCreation = MinHue; _minHueFromLastBitmapCreation = MinHue;
_maxHueFromLastBitmapCreation = MaxHue; _maxHueFromLastBitmapCreation = MaxHue;
_minSaturationFromLastBitmapCreation = MinSaturation; _minSaturationFromLastBitmapCreation = MinSaturation;
@ -1078,7 +1106,7 @@ namespace Avalonia.Controls.Primitives
double x, double x,
double y, double y,
Hsv baseHsv, Hsv baseHsv,
double minDimension, int minDimension,
ColorSpectrumComponents components, ColorSpectrumComponents components,
double minHue, double minHue,
double maxHue, double maxHue,
@ -1570,7 +1598,7 @@ namespace Avalonia.Controls.Primitives
displayedColor = Color; displayedColor = Color;
} }
var lum = ColorHelpers.GetRelativeLuminance(displayedColor); var lum = ColorHelper.GetRelativeLuminance(displayedColor);
return lum <= 0.5; return lum <= 0.5;
} }

0
src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs → src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs

0
src/Avalonia.Controls.ColorPicker/ColorSpectrumShape.cs → src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs

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

58
src/Avalonia.Controls.ColorPicker/Converters/ToColorConverter.cs

@ -0,0 +1,58 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converts the given value into a <see cref="Color"/> when a conversion is possible.
/// </summary>
public class ToColorConverter : IValueConverter
{
/// <inheritdoc/>
public object? Convert(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
if (value is Color valueColor)
{
return valueColor;
}
else if (value is HslColor valueHslColor)
{
return valueHslColor.ToRgb();
}
else if (value is HsvColor valueHsvColor)
{
return valueHsvColor.ToRgb();
}
else if (value is SolidColorBrush valueBrush)
{
// A brush may have an opacity set along with alpha transparency
double alpha = valueBrush.Color.A * valueBrush.Opacity;
return new Color(
(byte)MathUtilities.Clamp(alpha, 0x00, 0xFF),
valueBrush.Color.R,
valueBrush.Color.G,
valueBrush.Color.B);
}
return AvaloniaProperty.UnsetValue;
}
/// <inheritdoc/>
public object? ConvertBack(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
return AvaloniaProperty.UnsetValue;
}
}
}

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

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Avalonia.Controls.Primitives.Converters
{
/// <summary>
/// Converter to chain together multiple converters.
/// </summary>
public class ValueConverterGroup : List<IValueConverter>, IValueConverter
{
/// <inheritdoc/>
/// <inheritdoc/>
public object? Convert(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
object? curValue;
curValue = value;
for (int i = 0; i < Count; i++)
{
curValue = this[i].Convert(curValue, targetType, parameter, culture);
}
return curValue;
}
/// <inheritdoc/>
public object? ConvertBack(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{
object? curValue;
curValue = value;
for (int i = (Count - 1); i >= 0; i--)
{
curValue = this[i].ConvertBack(curValue, targetType, parameter, culture);
}
return curValue;
}
}
}

142
src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs

@ -0,0 +1,142 @@
using System;
using System.Globalization;
using System.Collections.Generic;
using Avalonia.Media;
using System.Text;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Contains helpers useful when working with colors.
/// </summary>
public static class ColorHelper
{
private static readonly Dictionary<Color, string> cachedDisplayNames = new Dictionary<Color, string>();
private static readonly object cacheMutex = new object();
/// <summary>
/// Gets the relative (perceptual) luminance/brightness of the given color.
/// 1 is closer to white while 0 is closer to black.
/// </summary>
/// <param name="color">The color to calculate relative luminance for.</param>
/// <returns>The relative (perceptual) luminance/brightness of the given color.</returns>
public static double GetRelativeLuminance(Color color)
{
// The equation for relative luminance is given by
//
// L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg
//
// where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise }
//
// If L is closer to 1, then the color is closer to white; if it is closer to 0,
// then the color is closer to black. This is based on the fact that the human
// eye perceives green to be much brighter than red, which in turn is perceived to be
// brighter than blue.
double rg = color.R <= 10 ? color.R / 3294.0 : Math.Pow(color.R / 269.0 + 0.0513, 2.4);
double gg = color.G <= 10 ? color.G / 3294.0 : Math.Pow(color.G / 269.0 + 0.0513, 2.4);
double bg = color.B <= 10 ? color.B / 3294.0 : Math.Pow(color.B / 269.0 + 0.0513, 2.4);
return (0.2126 * rg + 0.7152 * gg + 0.0722 * bg);
}
/// <summary>
/// Determines if color display names are supported based on the current thread culture.
/// </summary>
/// <remarks>
/// Only English names are currently supported following known color names.
/// In the future known color names could be localized.
/// </remarks>
public static bool ToDisplayNameExists
{
get => CultureInfo.CurrentUICulture.Name.StartsWith("EN", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines an approximate display name for the given color.
/// </summary>
/// <param name="color">The color to get the display name for.</param>
/// <returns>The approximate color display name.</returns>
public static string ToDisplayName(Color color)
{
// Without rounding, there are 16,777,216 possible RGB colors (without alpha).
// This is too many to cache and search through for performance reasons.
// It is also needlessly large as there are only ~140 known/named colors.
// Therefore, rounding of the input color's component values is done to
// reduce the color space into something more useful.
double rounding = 5;
var roundedColor = new Color(
0xFF,
Convert.ToByte(Math.Round(color.R / rounding) * rounding),
Convert.ToByte(Math.Round(color.G / rounding) * rounding),
Convert.ToByte(Math.Round(color.B / rounding) * rounding));
// Attempt to use a previously cached display name
lock (cacheMutex)
{
if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName))
{
return displayName;
}
}
// Find the closest known color by measuring 3D Euclidean distance (ignore alpha)
var closestKnownColor = KnownColor.None;
var closestKnownColorDistance = double.PositiveInfinity;
var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
for (int i = 1; i < knownColors.Length; i++) // Skip 'None'
{
// Transparent is skipped since alpha is ignored making it equivalent to White
if (knownColors[i] != KnownColor.Transparent)
{
Color knownColor = KnownColors.ToColor(knownColors[i]);
double distance = Math.Sqrt(
Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) +
Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) +
Math.Pow((double)(roundedColor.B - knownColor.B), 2.0));
if (distance < closestKnownColorDistance)
{
closestKnownColor = knownColors[i];
closestKnownColorDistance = distance;
}
}
}
// Return the closest known color as the display name
// Cache results for next time as well
if (closestKnownColor != KnownColor.None)
{
StringBuilder sb = new StringBuilder();
string name = closestKnownColor.ToString();
// Add spaces converting PascalCase to human-readable names
for (int i = 0; i < name.Length; i++)
{
if (i != 0 &&
char.IsUpper(name[i]))
{
sb.Append(' ');
}
sb.Append(name[i]);
}
string displayName = sb.ToString();
lock (cacheMutex)
{
cachedDisplayNames.Add(roundedColor, displayName);
}
return displayName;
}
else
{
return string.Empty;
}
}
}
}

629
src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs

@ -0,0 +1,629 @@
// 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 System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Contains internal, special-purpose helpers used with the color picker.
/// </summary>
internal static class ColorPickerHelpers
{
/// <summary>
/// Generates a new bitmap of the specified size by changing a specific color component.
/// This will produce a gradient representing a sweep of all possible values of the color component.
/// </summary>
/// <param name="width">The pixel width (X, horizontal) of the resulting bitmap.</param>
/// <param name="height">The pixel height (Y, vertical) of the resulting bitmap.</param>
/// <param name="orientation">The orientation of the resulting bitmap (gradient direction).</param>
/// <param name="colorModel">The color model being used: RGBA or HSVA.</param>
/// <param name="component">The specific color component to sweep.</param>
/// <param name="baseHsvColor">The base HSV color used for components not being changed.</param>
/// <param name="isAlphaMaxForced">Fix the alpha component value to maximum during calculation.
/// This will remove any alpha/transparency from the other component backgrounds.</param>
/// <param name="isSaturationValueMaxForced">Fix the saturation and value components to maximum
/// during calculation with the HSVA color model.
/// This will ensure colors are always discernible regardless of saturation/value.</param>
/// <returns>A new bitmap representing a gradient of color component values.</returns>
public static async Task<byte[]> CreateComponentBitmapAsync(
int width,
int height,
Orientation orientation,
ColorModel colorModel,
ColorComponent component,
HsvColor baseHsvColor,
bool isAlphaMaxForced,
bool isSaturationValueMaxForced)
{
if (width == 0 || height == 0)
{
return new byte[0];
}
var bitmap = await Task.Run<byte[]>(() =>
{
int pixelDataIndex = 0;
double componentStep;
byte[] bgraPixelData;
Color baseRgbColor = Colors.White;
Color rgbColor;
int bgraPixelDataHeight;
int bgraPixelDataWidth;
// Allocate the buffer
// BGRA formatted color components 1 byte each (4 bytes in a pixel)
bgraPixelData = new byte[width * height * 4];
bgraPixelDataHeight = height * 4;
bgraPixelDataWidth = width * 4;
// Maximize alpha component value
if (isAlphaMaxForced &&
component != ColorComponent.Alpha)
{
baseHsvColor = new HsvColor(1.0, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V);
}
// Convert HSV to RGB once
if (colorModel == ColorModel.Rgba)
{
baseRgbColor = baseHsvColor.ToRgb();
}
// Maximize Saturation and Value components when in HSVA mode
if (isSaturationValueMaxForced &&
colorModel == ColorModel.Hsva &&
component != ColorComponent.Alpha)
{
switch (component)
{
case ColorComponent.Component1:
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, 1.0);
break;
case ColorComponent.Component2:
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, 1.0);
break;
case ColorComponent.Component3:
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, baseHsvColor.V);
break;
}
}
// Create the color component gradient
if (orientation == Orientation.Horizontal)
{
// Determine the numerical increment of the color steps within the component
if (colorModel == ColorModel.Hsva)
{
if (component == ColorComponent.Component1)
{
componentStep = 360.0 / width;
}
else
{
componentStep = 1.0 / width;
}
}
else
{
componentStep = 255.0 / width;
}
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
if (y == 0)
{
rgbColor = GetColor(x * componentStep);
// Get a new color
bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255);
bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255);
bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255);
bgraPixelData[pixelDataIndex + 3] = rgbColor.A;
}
else
{
// Use the color in the row above
// Remember the pixel data is 1 dimensional instead of 2
bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex + 0 - bgraPixelDataWidth];
bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex + 1 - bgraPixelDataWidth];
bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex + 2 - bgraPixelDataWidth];
bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex + 3 - bgraPixelDataWidth];
}
pixelDataIndex += 4;
}
}
}
else
{
// Determine the numerical increment of the color steps within the component
if (colorModel == ColorModel.Hsva)
{
if (component == ColorComponent.Component1)
{
componentStep = 360.0 / height;
}
else
{
componentStep = 1.0 / height;
}
}
else
{
componentStep = 255.0 / height;
}
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
if (x == 0)
{
// The lowest component value should be at the 'bottom' of the bitmap
rgbColor = GetColor((height - 1 - y) * componentStep);
// Get a new color
bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255);
bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255);
bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255);
bgraPixelData[pixelDataIndex + 3] = rgbColor.A;
}
else
{
// Use the color in the column to the left
// Remember the pixel data is 1 dimensional instead of 2
bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex - 4];
bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex - 3];
bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex - 2];
bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex - 1];
}
pixelDataIndex += 4;
}
}
}
Color GetColor(double componentValue)
{
Color newRgbColor = Colors.White;
switch (component)
{
case ColorComponent.Component1:
{
if (colorModel == ColorModel.Hsva)
{
// Sweep hue
newRgbColor = HsvColor.ToRgb(
MathUtilities.Clamp(componentValue, 0.0, 360.0),
baseHsvColor.S,
baseHsvColor.V,
baseHsvColor.A);
}
else
{
// Sweep red
newRgbColor = new Color(
baseRgbColor.A,
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)),
baseRgbColor.G,
baseRgbColor.B);
}
break;
}
case ColorComponent.Component2:
{
if (colorModel == ColorModel.Hsva)
{
// Sweep saturation
newRgbColor = HsvColor.ToRgb(
baseHsvColor.H,
MathUtilities.Clamp(componentValue, 0.0, 1.0),
baseHsvColor.V,
baseHsvColor.A);
}
else
{
// Sweep green
newRgbColor = new Color(
baseRgbColor.A,
baseRgbColor.R,
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)),
baseRgbColor.B);
}
break;
}
case ColorComponent.Component3:
{
if (colorModel == ColorModel.Hsva)
{
// Sweep value
newRgbColor = HsvColor.ToRgb(
baseHsvColor.H,
baseHsvColor.S,
MathUtilities.Clamp(componentValue, 0.0, 1.0),
baseHsvColor.A);
}
else
{
// Sweep blue
newRgbColor = new Color(
baseRgbColor.A,
baseRgbColor.R,
baseRgbColor.G,
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)));
}
break;
}
case ColorComponent.Alpha:
{
if (colorModel == ColorModel.Hsva)
{
// Sweep alpha
newRgbColor = HsvColor.ToRgb(
baseHsvColor.H,
baseHsvColor.S,
baseHsvColor.V,
MathUtilities.Clamp(componentValue, 0.0, 1.0));
}
else
{
// Sweep alpha
newRgbColor = new Color(
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)),
baseRgbColor.R,
baseRgbColor.G,
baseRgbColor.B);
}
break;
}
}
return newRgbColor;
}
return bgraPixelData;
});
return bitmap;
}
public static Hsv IncrementColorComponent(
Hsv originalHsv,
HsvComponent component,
IncrementDirection direction,
IncrementAmount amount,
bool shouldWrap,
double minBound,
double maxBound)
{
Hsv newHsv = originalHsv;
if (amount == IncrementAmount.Small || !ColorHelper.ToDisplayNameExists)
{
// In order to avoid working with small values that can incur rounding issues,
// we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1.
newHsv.S *= 100;
newHsv.V *= 100;
// Note: *valueToIncrement replaced with ref local variable for C#, must be initialized
ref double valueToIncrement = ref newHsv.H;
double incrementAmount = 0.0;
// If we're adding a small increment, then we'll just add or subtract 1.
// If we're adding a large increment, then we want to snap to the next
// or previous major value - for hue, this is every increment of 30;
// for saturation and value, this is every increment of 10.
switch (component)
{
case HsvComponent.Hue:
valueToIncrement = ref newHsv.H;
incrementAmount = amount == IncrementAmount.Small ? 1 : 30;
break;
case HsvComponent.Saturation:
valueToIncrement = ref newHsv.S;
incrementAmount = amount == IncrementAmount.Small ? 1 : 10;
break;
case HsvComponent.Value:
valueToIncrement = ref newHsv.V;
incrementAmount = amount == IncrementAmount.Small ? 1 : 10;
break;
default:
throw new InvalidOperationException("Invalid HsvComponent.");
}
double previousValue = valueToIncrement;
valueToIncrement += (direction == IncrementDirection.Lower ? -incrementAmount : incrementAmount);
// If the value has reached outside the bounds, we were previous at the boundary, and we should wrap,
// then we'll place the selection on the other side of the spectrum.
// Otherwise, we'll place it on the boundary that was exceeded.
if (valueToIncrement < minBound)
{
valueToIncrement = (shouldWrap && previousValue == minBound) ? maxBound : minBound;
}
if (valueToIncrement > maxBound)
{
valueToIncrement = (shouldWrap && previousValue == maxBound) ? minBound : maxBound;
}
// We multiplied saturation and value by 100 previously, so now we want to put them back in the 0-1 range.
newHsv.S /= 100;
newHsv.V /= 100;
}
else
{
// While working with named colors, we're going to need to be working in actual HSV units,
// so we'll divide the min bound and max bound by 100 in the case of saturation or value,
// since we'll have received units between 0-100 and we need them within 0-1.
if (component == HsvComponent.Saturation ||
component == HsvComponent.Value)
{
minBound /= 100;
maxBound /= 100;
}
newHsv = FindNextNamedColor(originalHsv, component, direction, shouldWrap, minBound, maxBound);
}
return newHsv;
}
public static Hsv FindNextNamedColor(
Hsv originalHsv,
HsvComponent component,
IncrementDirection direction,
bool shouldWrap,
double minBound,
double maxBound)
{
// There's no easy way to directly get the next named color, so what we'll do
// is just iterate in the direction that we want to find it until we find a color
// in that direction that has a color name different than our current color name.
// Once we find a new color name, then we'll iterate across that color name until
// we find its bounds on the other side, and then select the color that is exactly
// in the middle of that color's bounds.
Hsv newHsv = originalHsv;
string originalColorName = ColorHelper.ToDisplayName(originalHsv.ToRgb().ToColor());
string newColorName = originalColorName;
// Note: *newValue replaced with ref local variable for C#, must be initialized
double originalValue = 0.0;
ref double newValue = ref newHsv.H;
double incrementAmount = 0.0;
switch (component)
{
case HsvComponent.Hue:
originalValue = originalHsv.H;
newValue = ref newHsv.H;
incrementAmount = 1;
break;
case HsvComponent.Saturation:
originalValue = originalHsv.S;
newValue = ref newHsv.S;
incrementAmount = 0.01;
break;
case HsvComponent.Value:
originalValue = originalHsv.V;
newValue = ref newHsv.V;
incrementAmount = 0.01;
break;
default:
throw new InvalidOperationException("Invalid HsvComponent.");
}
bool shouldFindMidPoint = true;
while (newColorName == originalColorName)
{
double previousValue = newValue;
newValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount;
bool justWrapped = false;
// If we've hit a boundary, then either we should wrap or we shouldn't.
// If we should, then we'll perform that wrapping if we were previously up against
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
if (newValue > maxBound)
{
if (shouldWrap)
{
newValue = minBound;
justWrapped = true;
}
else
{
newValue = maxBound;
shouldFindMidPoint = false;
newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor());
break;
}
}
else if (newValue < minBound)
{
if (shouldWrap)
{
newValue = maxBound;
justWrapped = true;
}
else
{
newValue = minBound;
shouldFindMidPoint = false;
newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor());
break;
}
}
if (!justWrapped &&
previousValue != originalValue &&
Math.Sign(newValue - originalValue) != Math.Sign(previousValue - originalValue))
{
// If we've wrapped all the way back to the start and have failed to find a new color name,
// then we'll just quit - there isn't a new color name that we're going to find.
shouldFindMidPoint = false;
break;
}
newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor());
}
if (shouldFindMidPoint)
{
Hsv startHsv = newHsv;
Hsv currentHsv = startHsv;
double startEndOffset = 0;
string currentColorName = newColorName;
// Note: *startValue/*currentValue replaced with ref local variables for C#, must be initialized
ref double startValue = ref startHsv.H;
ref double currentValue = ref currentHsv.H;
double wrapIncrement = 0;
switch (component)
{
case HsvComponent.Hue:
startValue = ref startHsv.H;
currentValue = ref currentHsv.H;
wrapIncrement = 360.0;
break;
case HsvComponent.Saturation:
startValue = ref startHsv.S;
currentValue = ref currentHsv.S;
wrapIncrement = 1.0;
break;
case HsvComponent.Value:
startValue = ref startHsv.V;
currentValue = ref currentHsv.V;
wrapIncrement = 1.0;
break;
default:
throw new InvalidOperationException("Invalid HsvComponent.");
}
while (newColorName == currentColorName)
{
currentValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount;
// If we've hit a boundary, then either we should wrap or we shouldn't.
// If we should, then we'll perform that wrapping if we were previously up against
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
if (currentValue > maxBound)
{
if (shouldWrap)
{
currentValue = minBound;
startEndOffset = maxBound - minBound;
}
else
{
currentValue = maxBound;
break;
}
}
else if (currentValue < minBound)
{
if (shouldWrap)
{
currentValue = maxBound;
startEndOffset = minBound - maxBound;
}
else
{
currentValue = minBound;
break;
}
}
currentColorName = ColorHelper.ToDisplayName(currentHsv.ToRgb().ToColor());
}
newValue = (startValue + currentValue + startEndOffset) / 2;
// Dividing by 2 may have gotten us halfway through a single step, so we'll
// remove that half-step if it exists.
double leftoverValue = Math.Abs(newValue);
while (leftoverValue > incrementAmount)
{
leftoverValue -= incrementAmount;
}
newValue -= leftoverValue;
while (newValue < minBound)
{
newValue += wrapIncrement;
}
while (newValue > maxBound)
{
newValue -= wrapIncrement;
}
}
return newHsv;
}
/// <summary>
/// Converts the given raw BGRA pre-multiplied alpha pixel data into a bitmap.
/// </summary>
/// <param name="bgraPixelData">The bitmap (in raw BGRA pre-multiplied alpha pixels).</param>
/// <param name="pixelWidth">The pixel width of the bitmap.</param>
/// <param name="pixelHeight">The pixel height of the bitmap.</param>
/// <returns>A new <see cref="WriteableBitmap"/>.</returns>
public static WriteableBitmap CreateBitmapFromPixelData(
IList<byte> bgraPixelData,
int pixelWidth,
int pixelHeight)
{
// Standard may need to change on some devices
Vector dpi = new Vector(96, 96);
var bitmap = new WriteableBitmap(
new PixelSize(pixelWidth, pixelHeight),
dpi,
PixelFormat.Bgra8888,
AlphaFormat.Premul);
// Warning: This is highly questionable
using (var frameBuffer = bitmap.Lock())
{
Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count);
}
return bitmap;
}
}
}

0
src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs → src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs

0
src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs → src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs

0
src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs → src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs

0
src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs → src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs

22
src/Avalonia.Controls.ColorPicker/HsvComponent.cs

@ -12,13 +12,21 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public enum HsvComponent public enum HsvComponent
{ {
/// <summary>
/// The Alpha component.
/// </summary>
/// <remarks>
/// Also see: <see cref="HsvColor.A"/>
/// </remarks>
Alpha = 0,
/// <summary> /// <summary>
/// The Hue component. /// The Hue component.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Also see: <see cref="HsvColor.H"/> /// Also see: <see cref="HsvColor.H"/>
/// </remarks> /// </remarks>
Hue, Hue = 1,
/// <summary> /// <summary>
/// The Saturation component. /// The Saturation component.
@ -26,7 +34,7 @@ namespace Avalonia.Controls
/// <remarks> /// <remarks>
/// Also see: <see cref="HsvColor.S"/> /// Also see: <see cref="HsvColor.S"/>
/// </remarks> /// </remarks>
Saturation, Saturation = 2,
/// <summary> /// <summary>
/// The Value component. /// The Value component.
@ -34,14 +42,6 @@ namespace Avalonia.Controls
/// <remarks> /// <remarks>
/// Also see: <see cref="HsvColor.V"/> /// Also see: <see cref="HsvColor.V"/>
/// </remarks> /// </remarks>
Value, Value = 3
/// <summary>
/// The Alpha component.
/// </summary>
/// <remarks>
/// Also see: <see cref="HsvColor.A"/>
/// </remarks>
Alpha
}; };
} }

42
src/Avalonia.Controls.ColorPicker/RgbComponent.cs

@ -0,0 +1,42 @@
using Avalonia.Media;
namespace Avalonia.Controls
{
/// <summary>
/// Defines a specific component in the RGB color model.
/// </summary>
public enum RgbComponent
{
/// <summary>
/// The Alpha component.
/// </summary>
/// <remarks>
/// Also see: <see cref="Color.A"/>
/// </remarks>
Alpha = 0,
/// <summary>
/// The Red component.
/// </summary>
/// <remarks>
/// Also see: <see cref="Color.R"/>
/// </remarks>
Red = 1,
/// <summary>
/// The Green component.
/// </summary>
/// <remarks>
/// Also see: <see cref="Color.G"/>
/// </remarks>
Green = 2,
/// <summary>
/// The Blue component.
/// </summary>
/// <remarks>
/// Also see: <see cref="Color.B"/>
/// </remarks>
Blue = 3
};
}

86
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml

@ -0,0 +1,86 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:Avalonia.Controls.Converters"
xmlns:pc="using:Avalonia.Controls.Primitives.Converters"
x:CompileBindings="True">
<Styles.Resources>
<pc:AccentColorConverter x:Key="AccentColor" />
<converters:ToBrushConverter x:Key="ToBrush" />
<converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/>
<converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/>
</Styles.Resources>
<Style Selector="ColorPreviewer">
<Setter Property="Height" Value="70" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Left accent colors -->
<Grid Grid.Column="0"
Height="40"
Width="80"
ColumnDefinitions="*,*"
Margin="0,0,-10,0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding ShowAccentColors}">
<Border Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{StaticResource CheckeredBackgroundBrush}" />
<Border x:Name="AccentDec2Border"
Grid.Column="0"
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}"
Tag="-2"
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" />
<Border x:Name="AccentDec1Border"
Grid.Column="1"
Tag="-1"
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-1'}" />
</Grid>
<!-- Right accent colors -->
<Grid Grid.Column="2"
Height="40"
Width="80"
ColumnDefinitions="*,*"
Margin="-10,0,0,0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding ShowAccentColors}">
<Border Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{StaticResource CheckeredBackgroundBrush}" />
<Border x:Name="AccentInc1Border"
Grid.Column="0"
Tag="1"
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='1'}" />
<Border x:Name="AccentInc2Border"
Grid.Column="1"
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource RightCornerRadiusFilterConverter}}"
Tag="2"
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='2'}" />
</Grid>
<!-- Must be last for drop shadow Z-index -->
<Border Grid.Column="1"
BoxShadow="0 0 10 2 #BF000000"
CornerRadius="{TemplateBinding CornerRadius}"
Margin="10">
<Panel>
<Border Background="{StaticResource CheckeredBackgroundBrush}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Border x:Name="PreviewBorder"
CornerRadius="{TemplateBinding CornerRadius}"
Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrush}}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Panel>
</Border>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

194
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml

@ -0,0 +1,194 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:Avalonia.Controls.Converters"
x:CompileBindings="True">
<Styles.Resources>
<converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" />
<converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" />
</Styles.Resources>
<Style Selector="Thumb.ColorSliderThumbStyle">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style Selector="ColorSlider:horizontal">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Height" Value="20" />
<Setter Property="Template">
<ControlTemplate>
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{StaticResource CheckeredBackgroundBrush}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Track Name="PART_Track"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
Orientation="Horizontal">
<Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.DecreaseButton>
<Track.IncreaseButton>
<RepeatButton Name="PART_IncreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.IncreaseButton>
<Thumb Classes="ColorSliderThumbStyle"
Name="ColorSliderThumb"
Margin="0"
Padding="0"
DataContext="{TemplateBinding Value}"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Height}" />
</Track>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ColorSlider:vertical">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Width" Value="20" />
<Setter Property="Template">
<ControlTemplate>
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{StaticResource CheckeredBackgroundBrush}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Track Name="PART_Track"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
Orientation="Vertical">
<Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.DecreaseButton>
<Track.IncreaseButton>
<RepeatButton Name="PART_IncreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.IncreaseButton>
<Thumb Classes="ColorSliderThumbStyle"
Name="ColorSliderThumb"
Margin="0"
Padding="0"
DataContext="{TemplateBinding Value}"
Height="{TemplateBinding Width}"
Width="{TemplateBinding Width}" />
</Track>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<!-- Normal State -->
<Style Selector="ColorSlider /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="BorderThickness" Value="3" />
</Style>
<!-- Selector/Thumb Color -->
<Style Selector="ColorSlider:pointerover /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="Opacity" Value="0.75" />
</Style>
<Style Selector="ColorSlider:pointerover:dark-selector /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="Opacity" Value="0.7" />
</Style>
<Style Selector="ColorSlider:pointerover:light-selector /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="Opacity" Value="0.8" />
</Style>
<Style Selector="ColorSlider:dark-selector /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="BorderBrush" Value="Black" />
</Style>
<Style Selector="ColorSlider:light-selector /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="BorderBrush" Value="White" />
</Style>
</Styles>

22
src/Avalonia.Controls.ColorPicker/Themes/Default.xaml → src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml

@ -1,7 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui" <Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True" xmlns:converters="using:Avalonia.Controls.Converters"
xmlns:converters="using:Avalonia.Controls.Converters"> x:CompileBindings="True">
<Styles.Resources> <Styles.Resources>
<converters:EnumValueEqualsConverter x:Key="EnumValueEquals" /> <converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
@ -48,7 +48,10 @@
Background="Transparent" Background="Transparent"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<Panel x:Name="PART_SelectionEllipsePanel"> <!-- Note: ToolTip.VerticalOffset is for touch devices to keep the tip above fingers -->
<Panel x:Name="PART_SelectionEllipsePanel"
ToolTip.VerticalOffset="-10"
ToolTip.Placement="Top">
<Ellipse x:Name="FocusEllipse" <Ellipse x:Name="FocusEllipse"
Margin="-2" Margin="-2"
StrokeThickness="2" StrokeThickness="2"
@ -59,13 +62,10 @@
StrokeThickness="2" StrokeThickness="2"
IsHitTestVisible="False" IsHitTestVisible="False"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch" />
ToolTip.VerticalOffset="-20" <ToolTip.Tip>
ToolTip.Placement="Top"> <!-- Set in code-behind -->
<ToolTip.Tip> </ToolTip.Tip>
<ToolTip x:Name="PART_ColorNameToolTip" />
</ToolTip.Tip>
</Ellipse>
</Panel> </Panel>
</Canvas> </Canvas>
<Rectangle x:Name="BorderRectangle" <Rectangle x:Name="BorderRectangle"

28
src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml

@ -0,0 +1,28 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<VisualBrush x:Key="CheckeredBackgroundBrush"
TileMode="Tile"
Stretch="Uniform"
DestinationRect="0,0,8,8">
<VisualBrush.Visual>
<DrawingPresenter Width="8"
Height="8">
<DrawingGroup>
<GeometryDrawing Geometry="M0,0 L2,0 2,2, 0,2Z"
Brush="Transparent" />
<GeometryDrawing Geometry="M0,1 L2,1 2,2, 1,2 1,0 0,0Z"
Brush="#19808080" />
</DrawingGroup>
</DrawingPresenter>
</VisualBrush.Visual>
</VisualBrush>
</Styles.Resources>
<!-- Primitives -->
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml" />
</Styles>

86
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml

@ -0,0 +1,86 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:Avalonia.Controls.Converters"
xmlns:pc="using:Avalonia.Controls.Primitives.Converters"
x:CompileBindings="True">
<Styles.Resources>
<pc:AccentColorConverter x:Key="AccentColor" />
<converters:ToBrushConverter x:Key="ToBrush" />
<converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/>
<converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/>
</Styles.Resources>
<Style Selector="ColorPreviewer">
<Setter Property="Height" Value="70" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Left accent colors -->
<Grid Grid.Column="0"
Height="40"
Width="80"
ColumnDefinitions="*,*"
Margin="0,0,-10,0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding ShowAccentColors}">
<Border Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{StaticResource CheckeredBackgroundBrush}" />
<Border x:Name="AccentDec2Border"
Grid.Column="0"
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}"
Tag="-2"
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" />
<Border x:Name="AccentDec1Border"
Grid.Column="1"
Tag="-1"
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-1'}" />
</Grid>
<!-- Right accent colors -->
<Grid Grid.Column="2"
Height="40"
Width="80"
ColumnDefinitions="*,*"
Margin="-10,0,0,0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding ShowAccentColors}">
<Border Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{StaticResource CheckeredBackgroundBrush}" />
<Border x:Name="AccentInc1Border"
Grid.Column="0"
Tag="1"
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='1'}" />
<Border x:Name="AccentInc2Border"
Grid.Column="1"
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource RightCornerRadiusFilterConverter}}"
Tag="2"
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='2'}" />
</Grid>
<!-- Must be last for drop shadow Z-index -->
<Border Grid.Column="1"
BoxShadow="0 0 10 2 #BF000000"
CornerRadius="{TemplateBinding CornerRadius}"
Margin="10">
<Panel>
<Border Background="{StaticResource CheckeredBackgroundBrush}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Border x:Name="PreviewBorder"
CornerRadius="{TemplateBinding CornerRadius}"
Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrush}}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Panel>
</Border>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</Styles>

194
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml

@ -0,0 +1,194 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:Avalonia.Controls.Converters"
x:CompileBindings="True">
<Styles.Resources>
<converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" />
<converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" />
</Styles.Resources>
<Style Selector="Thumb.ColorSliderThumbStyle">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style Selector="ColorSlider:horizontal">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Height" Value="20" />
<Setter Property="Template">
<ControlTemplate>
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{StaticResource CheckeredBackgroundBrush}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Track Name="PART_Track"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
Orientation="Horizontal">
<Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.DecreaseButton>
<Track.IncreaseButton>
<RepeatButton Name="PART_IncreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.IncreaseButton>
<Thumb Classes="ColorSliderThumbStyle"
Name="ColorSliderThumb"
Margin="0"
Padding="0"
DataContext="{TemplateBinding Value}"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Height}" />
</Track>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ColorSlider:vertical">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Width" Value="20" />
<Setter Property="Template">
<ControlTemplate>
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{StaticResource CheckeredBackgroundBrush}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Rectangle HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Track Name="PART_Track"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
Orientation="Vertical">
<Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.DecreaseButton>
<Track.IncreaseButton>
<RepeatButton Name="PART_IncreaseButton"
Background="Transparent"
Focusable="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<RepeatButton.Template>
<ControlTemplate>
<Border Name="FocusTarget"
Background="Transparent"
Margin="0,-10" />
</ControlTemplate>
</RepeatButton.Template>
</RepeatButton>
</Track.IncreaseButton>
<Thumb Classes="ColorSliderThumbStyle"
Name="ColorSliderThumb"
Margin="0"
Padding="0"
DataContext="{TemplateBinding Value}"
Height="{TemplateBinding Width}"
Width="{TemplateBinding Width}" />
</Track>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<!-- Normal State -->
<Style Selector="ColorSlider /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="BorderThickness" Value="3" />
</Style>
<!-- Selector/Thumb Color -->
<Style Selector="ColorSlider:pointerover /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="Opacity" Value="0.75" />
</Style>
<Style Selector="ColorSlider:pointerover:dark-selector /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="Opacity" Value="0.7" />
</Style>
<Style Selector="ColorSlider:pointerover:light-selector /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="Opacity" Value="0.8" />
</Style>
<Style Selector="ColorSlider:dark-selector /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlBackgroundChromeBlackHighBrush}" />
</Style>
<Style Selector="ColorSlider:light-selector /template/ Thumb.ColorSliderThumbStyle">
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlBackgroundChromeWhiteBrush}" />
</Style>
</Styles>

25
src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml → src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml

@ -1,7 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui" <Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True" xmlns:converters="using:Avalonia.Controls.Converters"
xmlns:converters="using:Avalonia.Controls.Converters"> x:CompileBindings="True">
<Styles.Resources> <Styles.Resources>
<converters:EnumValueEqualsConverter x:Key="EnumValueEquals" /> <converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
@ -48,7 +48,10 @@
Background="Transparent" Background="Transparent"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<Panel x:Name="PART_SelectionEllipsePanel"> <!-- Note: ToolTip.VerticalOffset is for touch devices to keep the tip above fingers -->
<Panel x:Name="PART_SelectionEllipsePanel"
ToolTip.VerticalOffset="-10"
ToolTip.Placement="Top">
<Ellipse x:Name="FocusEllipse" <Ellipse x:Name="FocusEllipse"
Margin="-2" Margin="-2"
StrokeThickness="2" StrokeThickness="2"
@ -59,13 +62,10 @@
StrokeThickness="2" StrokeThickness="2"
IsHitTestVisible="False" IsHitTestVisible="False"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch" />
ToolTip.VerticalOffset="-20" <ToolTip.Tip>
ToolTip.Placement="Top"> <!-- Set in code-behind -->
<ToolTip.Tip> </ToolTip.Tip>
<ToolTip x:Name="PART_ColorNameToolTip" />
</ToolTip.Tip>
</Ellipse>
</Panel> </Panel>
</Canvas> </Canvas>
<Rectangle x:Name="BorderRectangle" <Rectangle x:Name="BorderRectangle"
@ -118,6 +118,9 @@
</Style> </Style>
<Style Selector="ColorSpectrum:pointerover /template/ Ellipse#SelectionEllipse"> <Style Selector="ColorSpectrum:pointerover /template/ Ellipse#SelectionEllipse">
<Setter Property="Opacity" Value="0.7" />
</Style>
<Style Selector="ColorSpectrum:pointerover:light-selector /template/ Ellipse#SelectionEllipse">
<Setter Property="Opacity" Value="0.8" /> <Setter Property="Opacity" Value="0.8" />
</Style> </Style>

28
src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml

@ -0,0 +1,28 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<VisualBrush x:Key="CheckeredBackgroundBrush"
TileMode="Tile"
Stretch="Uniform"
DestinationRect="0,0,8,8">
<VisualBrush.Visual>
<DrawingPresenter Width="8"
Height="8">
<DrawingGroup>
<GeometryDrawing Geometry="M0,0 L2,0 2,2, 0,2Z"
Brush="Transparent" />
<GeometryDrawing Geometry="M0,1 L2,1 2,2, 1,2 1,0 0,0Z"
Brush="#19808080" />
</DrawingGroup>
</DrawingPresenter>
</VisualBrush.Visual>
</VisualBrush>
</Styles.Resources>
<!-- Primitives -->
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml" />
</Styles>

15
src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
namespace Avalonia.Controls.Converters namespace Avalonia.Controls.Converters
@ -22,7 +21,12 @@ namespace Avalonia.Controls.Converters
/// </summary> /// </summary>
public double Scale { get; set; } = 1; public double Scale { get; set; } = 1;
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) /// <inheritdoc/>
public object? Convert(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{ {
if (!(value is CornerRadius radius)) if (!(value is CornerRadius radius))
{ {
@ -36,7 +40,12 @@ namespace Avalonia.Controls.Converters
Filter.HasAllFlags(Corners.BottomLeft) ? radius.BottomLeft * Scale : 0); Filter.HasAllFlags(Corners.BottomLeft) ? radius.BottomLeft * Scale : 0);
} }
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) /// <inheritdoc/>
public object? ConvertBack(
object? value,
Type targetType,
object? parameter,
CultureInfo culture)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

25
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -511,28 +511,39 @@ namespace Avalonia.Controls.Presenters
InvalidateMeasure(); InvalidateMeasure();
} }
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
if (string.IsNullOrEmpty(Text))
{
return new Size();
}
_constraint = availableSize; _constraint = availableSize;
_textLayout = null; _textLayout = null;
InvalidateArrange(); InvalidateArrange();
var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); var measuredSize = TextLayout.Bounds.Size;
return new Size(measuredSize.Width, measuredSize.Height); return measuredSize;
} }
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
if (finalSize.Width < TextLayout.Bounds.Width)
{
finalSize = finalSize.WithWidth(TextLayout.Bounds.Width);
}
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
{ {
return finalSize; return finalSize;
} }
_constraint = new Size(finalSize.Width, Math.Ceiling(finalSize.Height)); _constraint = new Size(finalSize.Width, double.PositiveInfinity);
_textLayout = null; _textLayout = null;

56
src/Avalonia.Controls/Primitives/IPopupHost.cs

@ -2,6 +2,7 @@ using System;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Media;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
@ -17,16 +18,50 @@ namespace Avalonia.Controls.Primitives
public interface IPopupHost : IDisposable, IFocusScope public interface IPopupHost : IDisposable, IFocusScope
{ {
/// <summary> /// <summary>
/// Sets the control to display in the popup. /// Gets or sets the fixed width of the popup.
/// </summary> /// </summary>
/// <param name="control"></param> double Width { get; set; }
void SetChild(IControl? control);
/// <summary>
/// Gets or sets the minimum width of the popup.
/// </summary>
double MinWidth { get; set; }
/// <summary>
/// Gets or sets the maximum width of the popup.
/// </summary>
double MaxWidth { get; set; }
/// <summary>
/// Gets or sets the fixed height of the popup.
/// </summary>
double Height { get; set; }
/// <summary>
/// Gets or sets the minimum height of the popup.
/// </summary>
double MinHeight { get; set; }
/// <summary>
/// Gets or sets the maximum height of the popup.
/// </summary>
double MaxHeight { get; set; }
/// <summary> /// <summary>
/// Gets the presenter from the control's template. /// Gets the presenter from the control's template.
/// </summary> /// </summary>
IContentPresenter? Presenter { get; } IContentPresenter? Presenter { get; }
/// <summary>
/// Gets or sets whether the popup appears on top of all other windows.
/// </summary>
bool Topmost { get; set; }
/// <summary>
/// Gets or sets a transform that will be applied to the popup.
/// </summary>
Transform? Transform { get; set; }
/// <summary> /// <summary>
/// Gets the root of the visual tree in the case where the popup is presented using a /// Gets the root of the visual tree in the case where the popup is presented using a
/// separate visual tree. /// separate visual tree.
@ -57,6 +92,12 @@ namespace Avalonia.Controls.Primitives
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All, PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
Rect? rect = null); Rect? rect = null);
/// <summary>
/// Sets the control to display in the popup.
/// </summary>
/// <param name="control"></param>
void SetChild(IControl? control);
/// <summary> /// <summary>
/// Shows the popup. /// Shows the popup.
/// </summary> /// </summary>
@ -66,14 +107,5 @@ namespace Avalonia.Controls.Primitives
/// Hides the popup. /// Hides the popup.
/// </summary> /// </summary>
void Hide(); void Hide();
/// <summary>
/// Binds the constraints of the popup host to a set of properties, usally those present on
/// <see cref="Popup"/>.
/// </summary>
IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty,
StyledProperty<double> minWidthProperty, StyledProperty<double> maxWidthProperty,
StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty);
} }
} }

42
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@ -11,6 +11,12 @@ namespace Avalonia.Controls.Primitives
{ {
public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup
{ {
/// <summary>
/// Defines the <see cref="Transform"/> property.
/// </summary>
public static readonly StyledProperty<Transform?> TransformProperty =
PopupRoot.TransformProperty.AddOwner<OverlayPopupHost>();
private readonly OverlayLayer _overlayLayer; private readonly OverlayLayer _overlayLayer;
private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters(); private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters();
private ManagedPopupPositioner _positioner; private ManagedPopupPositioner _positioner;
@ -29,10 +35,22 @@ namespace Avalonia.Controls.Primitives
} }
public IVisual? HostedVisualTreeRoot => null; public IVisual? HostedVisualTreeRoot => null;
public Transform? Transform
{
get => GetValue(TransformProperty);
set => SetValue(TransformProperty, value);
}
/// <inheritdoc/> /// <inheritdoc/>
IInteractive? IInteractive.InteractiveParent => Parent; IInteractive? IInteractive.InteractiveParent => Parent;
bool IPopupHost.Topmost
{
get => false;
set { /* Not currently supported in overlay popups */ }
}
public void Dispose() => Hide(); public void Dispose() => Hide();
@ -48,28 +66,6 @@ namespace Avalonia.Controls.Primitives
_shown = false; _shown = false;
} }
public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty, StyledProperty<double> minWidthProperty,
StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty)
{
// Topmost property is not supported
var bindings = new List<IDisposable>();
void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to]));
Bind(WidthProperty, widthProperty);
Bind(MinWidthProperty, minWidthProperty);
Bind(MaxWidthProperty, maxWidthProperty);
Bind(HeightProperty, heightProperty);
Bind(MinHeightProperty, minHeightProperty);
Bind(MaxHeightProperty, maxHeightProperty);
return Disposable.Create(() =>
{
foreach (var x in bindings)
x.Dispose();
});
}
public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None, PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None,
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All, PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,

150
src/Avalonia.Controls/Primitives/Popup.cs

@ -14,6 +14,8 @@ using Avalonia.LogicalTree;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -33,6 +35,12 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<Control?> ChildProperty = public static readonly StyledProperty<Control?> ChildProperty =
AvaloniaProperty.Register<Popup, Control?>(nameof(Child)); AvaloniaProperty.Register<Popup, Control?>(nameof(Child));
/// <summary>
/// Defines the <see cref="InheritsTransform"/> property.
/// </summary>
public static readonly StyledProperty<bool> InheritsTransformProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(InheritsTransform));
/// <summary> /// <summary>
/// Defines the <see cref="IsOpen"/> property. /// Defines the <see cref="IsOpen"/> property.
/// </summary> /// </summary>
@ -196,6 +204,16 @@ namespace Avalonia.Controls.Primitives
set; set;
} }
/// <summary>
/// Gets or sets a value that determines whether the popup inherits the render transform
/// from its <see cref="PlacementTarget"/>. Defaults to false.
/// </summary>
public bool InheritsTransform
{
get => GetValue(InheritsTransformProperty);
set => SetValue(InheritsTransformProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets a value that determines how the <see cref="Popup"/> can be dismissed. /// Gets or sets a value that determines how the <see cref="Popup"/> can be dismissed.
/// </summary> /// </summary>
@ -395,24 +413,29 @@ namespace Avalonia.Controls.Primitives
} }
_isOpenRequested = false; _isOpenRequested = false;
var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
var handlerCleanup = new CompositeDisposable(7); var handlerCleanup = new CompositeDisposable(7);
popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, UpdateHostSizing(popupHost, topLevel, placementTarget);
HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup); popupHost.Topmost = Topmost;
popupHost.SetChild(Child); popupHost.SetChild(Child);
((ISetLogicalParent)popupHost).SetParent(this); ((ISetLogicalParent)popupHost).SetParent(this);
popupHost.ConfigurePosition( if (InheritsTransform && placementTarget is Control c)
placementTarget, {
PlacementMode, SubscribeToEventHandler<Control, EventHandler<AvaloniaPropertyChangedEventArgs>>(
new Point(HorizontalOffset, VerticalOffset), c,
PlacementAnchor, PlacementTargetPropertyChanged,
PlacementGravity, (x, handler) => x.PropertyChanged += handler,
PlacementConstraintAdjustment, (x, handler) => x.PropertyChanged -= handler).DisposeWith(handlerCleanup);
PlacementRect); }
else
{
popupHost.Transform = null;
}
UpdateHostPosition(popupHost, placementTarget);
SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied, SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler, (x, handler) => x.TemplateApplied += handler,
@ -494,7 +517,7 @@ namespace Avalonia.Controls.Primitives
} }
} }
_openState = new PopupOpenState(topLevel, popupHost, cleanupPopup); _openState = new PopupOpenState(placementTarget, topLevel, popupHost, cleanupPopup);
WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint); WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint);
@ -542,7 +565,93 @@ namespace Avalonia.Controls.Primitives
base.OnDetachedFromLogicalTree(e); base.OnDetachedFromLogicalTree(e);
Close(); Close();
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (_openState is not null)
{
if (change.Property == WidthProperty ||
change.Property == MinWidthProperty ||
change.Property == MaxWidthProperty ||
change.Property == HeightProperty ||
change.Property == MinHeightProperty ||
change.Property == MaxHeightProperty)
{
UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
}
else if (change.Property == PlacementTargetProperty ||
change.Property == PlacementModeProperty ||
change.Property == HorizontalOffsetProperty ||
change.Property == VerticalOffsetProperty ||
change.Property == PlacementAnchorProperty ||
change.Property == PlacementConstraintAdjustmentProperty ||
change.Property == PlacementRectProperty)
{
if (change.Property == PlacementTargetProperty)
{
var newTarget = change.GetNewValue<Control?>() ?? this.FindLogicalAncestorOfType<IControl>();
if (newTarget is null || newTarget.GetVisualRoot() != _openState.TopLevel)
{
Close();
return;
}
_openState.PlacementTarget = newTarget;
}
UpdateHostPosition(_openState.PopupHost, _openState.PlacementTarget);
}
else if (change.Property == TopmostProperty)
{
_openState.PopupHost.Topmost = change.GetNewValue<bool>();
}
}
}
private void UpdateHostPosition(IPopupHost popupHost, IControl placementTarget)
{
popupHost.ConfigurePosition(
placementTarget,
PlacementMode,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
PlacementRect ?? new Rect(default, placementTarget.Bounds.Size));
}
private void UpdateHostSizing(IPopupHost popupHost, TopLevel topLevel, IControl placementTarget)
{
var scaleX = 1.0;
var scaleY = 1.0;
if (InheritsTransform && placementTarget.TransformToVisual(topLevel) is Matrix m)
{
scaleX = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12);
scaleY = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12);
// Ideally we'd only assign a ScaleTransform here when the scale != 1, but there's
// an issue with LayoutTransformControl in that it sets its LayoutTransform property
// with LocalValue priority in ArrangeOverride in certain cases when LayoutTransform
// is null, which breaks TemplateBindings to this property. Offending commit/line:
//
// https://github.com/AvaloniaUI/Avalonia/commit/6fbe1c2180ef45a940e193f1b4637e64eaab80ed#diff-5344e793df13f462126a8153ef46c44194f244b6890f25501709bae51df97f82R54
popupHost.Transform = new ScaleTransform(scaleX, scaleY);
}
else
{
popupHost.Transform = null;
}
popupHost.Width = Width * scaleX;
popupHost.MinWidth = MinWidth * scaleX;
popupHost.MaxWidth = MaxWidth * scaleX;
popupHost.Height = Height * scaleY;
popupHost.MinHeight = MinHeight * scaleY;
popupHost.MaxHeight = MaxHeight * scaleY;
}
private void HandlePositionChange() private void HandlePositionChange()
{ {
if (_openState != null) if (_openState != null)
@ -824,6 +933,14 @@ namespace Avalonia.Controls.Primitives
} }
} }
private void PlacementTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (_openState is not null && e.Property == Visual.TransformedBoundsProperty)
{
UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
}
}
private void WindowLostFocus() private void WindowLostFocus()
{ {
if (IsLightDismissEnabled) if (IsLightDismissEnabled)
@ -862,15 +979,16 @@ namespace Avalonia.Controls.Primitives
private readonly IDisposable _cleanup; private readonly IDisposable _cleanup;
private IDisposable? _presenterCleanup; private IDisposable? _presenterCleanup;
public PopupOpenState(TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup) public PopupOpenState(IControl placementTarget, TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup)
{ {
PlacementTarget = placementTarget;
TopLevel = topLevel; TopLevel = topLevel;
PopupHost = popupHost; PopupHost = popupHost;
_cleanup = cleanup; _cleanup = cleanup;
} }
public TopLevel TopLevel { get; } public TopLevel TopLevel { get; }
public IControl PlacementTarget { get; set; }
public IPopupHost PopupHost { get; } public IPopupHost PopupHost { get; }
public void SetPresenterSubscription(IDisposable? presenterCleanup) public void SetPresenterSubscription(IDisposable? presenterCleanup)

39
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -1,6 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -8,7 +6,6 @@ using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using JetBrains.Annotations;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -17,6 +14,12 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost
{ {
/// <summary>
/// Defines the <see cref="Transform"/> property.
/// </summary>
public static readonly StyledProperty<Transform?> TransformProperty =
AvaloniaProperty.Register<PopupRoot, Transform?>(nameof(Transform));
private PopupPositionerParameters _positionerParameters; private PopupPositionerParameters _positionerParameters;
/// <summary> /// <summary>
@ -54,6 +57,15 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public new IPopupImpl? PlatformImpl => (IPopupImpl?)base.PlatformImpl; public new IPopupImpl? PlatformImpl => (IPopupImpl?)base.PlatformImpl;
/// <summary>
/// Gets or sets a transform that will be applied to the popup.
/// </summary>
public Transform? Transform
{
get => GetValue(TransformProperty);
set => SetValue(TransformProperty, value);
}
/// <summary> /// <summary>
/// Gets the parent control in the event route. /// Gets the parent control in the event route.
/// </summary> /// </summary>
@ -103,27 +115,6 @@ namespace Avalonia.Controls.Primitives
IVisual IPopupHost.HostedVisualTreeRoot => this; IVisual IPopupHost.HostedVisualTreeRoot => this;
public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty, StyledProperty<double> minWidthProperty,
StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty)
{
var bindings = new List<IDisposable>();
void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to]));
Bind(WidthProperty, widthProperty);
Bind(MinWidthProperty, minWidthProperty);
Bind(MaxWidthProperty, maxWidthProperty);
Bind(HeightProperty, heightProperty);
Bind(MinHeightProperty, minHeightProperty);
Bind(MaxHeightProperty, maxHeightProperty);
Bind(TopmostProperty, topmostProperty);
return Disposable.Create(() =>
{
foreach (var x in bindings)
x.Dispose();
});
}
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity; var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;

19
src/Avalonia.Controls/TextBlock.cs

@ -604,7 +604,9 @@ namespace Avalonia.Controls
return new Size(); return new Size();
} }
var padding = Padding; var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
_constraint = availableSize.Deflate(padding); _constraint = availableSize.Deflate(padding);
@ -612,23 +614,24 @@ namespace Avalonia.Controls
InvalidateArrange(); InvalidateArrange();
var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); var measuredSize = TextLayout.Bounds.Size.Inflate(padding);
return new Size(measuredSize.Width, measuredSize.Height).Inflate(padding); return measuredSize;
} }
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
if(finalSize.Width < TextLayout.Bounds.Width)
{
finalSize = finalSize.WithWidth(TextLayout.Bounds.Width);
}
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
{ {
return finalSize; return finalSize;
} }
var padding = Padding; _constraint = new Size(finalSize.Width, double.PositiveInfinity);
var textSize = finalSize.Deflate(padding);
_constraint = new Size(textSize.Width, Math.Ceiling(textSize.Height));
_textLayout = null; _textLayout = null;

64
src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs

@ -1,13 +1,17 @@
using System;
using Avalonia.Data;
using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class AvaloniaPropertyViewModel : PropertyViewModel internal class AvaloniaPropertyViewModel : PropertyViewModel
{ {
private readonly AvaloniaObject _target; private readonly AvaloniaObject _target;
private System.Type _assignedType; private Type _assignedType;
private object? _value; private object? _value;
private string _priority; private string _priority;
private string _group; private string _group;
private readonly System.Type _propertyType; private readonly Type _propertyType;
#nullable disable #nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI. // Remove "nullable disable" after MemberNotNull will work on our CI.
@ -28,13 +32,9 @@ namespace Avalonia.Diagnostics.ViewModels
public AvaloniaProperty Property { get; } public AvaloniaProperty Property { get; }
public override object Key => Property; public override object Key => Property;
public override string Name { get; } public override string Name { get; }
public override bool? IsAttached => public override bool? IsAttached => Property.IsAttached;
Property.IsAttached; public override string Priority => _priority;
public override Type AssignedType => _assignedType;
public override string Priority =>
_priority;
public override System.Type AssignedType => _assignedType;
public override string? Value public override string? Value
{ {
@ -53,30 +53,58 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Group => _group; public override string Group => _group;
public override System.Type? DeclaringType { get; } public override Type? DeclaringType { get; }
public override System.Type PropertyType => _propertyType; public override Type PropertyType => _propertyType;
// [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update() public override void Update()
{ {
if (Property.IsDirect) if (Property.IsDirect)
{ {
RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); object? value;
RaiseAndSetIfChanged(ref _assignedType,_value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); Type? valueType = null;
try
{
value = _target.GetValue(Property);
valueType = value?.GetType();
}
catch (Exception e)
{
value = e.GetBaseException();
}
RaiseAndSetIfChanged(ref _value, value, nameof(Value));
RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType));
RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority));
_group = "Properties"; _group = "Properties";
} }
else else
{ {
var val = _target.GetDiagnostic(Property); object? value;
Type? valueType = null;
BindingPriority? priority = null;
try
{
var diag = _target.GetDiagnostic(Property);
value = diag.Value;
valueType = value?.GetType();
priority = diag.Priority;
}
catch (Exception e)
{
value = e.GetBaseException();
}
RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); RaiseAndSetIfChanged(ref _value, value, nameof(Value));
RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType));
if (val != null) if (priority != null)
{ {
RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority)); RaiseAndSetIfChanged(ref _priority, priority.ToString()!, nameof(Priority));
RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group)); RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group));
} }
else else

41
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs

@ -1,13 +1,15 @@
using System.Reflection; using System;
using System.Reflection;
using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class ClrPropertyViewModel : PropertyViewModel internal class ClrPropertyViewModel : PropertyViewModel
{ {
private readonly object _target; private readonly object _target;
private System.Type _assignedType; private Type _assignedType;
private object? _value; private object? _value;
private readonly System.Type _propertyType; private readonly Type _propertyType;
#nullable disable #nullable disable
// Remove "nullable disable" after MemberNotNull will work on our CI. // Remove "nullable disable" after MemberNotNull will work on our CI.
@ -25,6 +27,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
Name = property.DeclaringType.Name + '.' + property.Name; Name = property.DeclaringType.Name + '.' + property.Name;
} }
DeclaringType = property.DeclaringType; DeclaringType = property.DeclaringType;
_propertyType = property.PropertyType; _propertyType = property.PropertyType;
@ -36,10 +39,10 @@ namespace Avalonia.Diagnostics.ViewModels
public override string Name { get; } public override string Name { get; }
public override string Group => "CLR Properties"; public override string Group => "CLR Properties";
public override System.Type AssignedType => _assignedType; public override Type AssignedType => _assignedType;
public override System.Type PropertyType => _propertyType; public override Type PropertyType => _propertyType;
public override string? Value public override string? Value
{ {
get => ConvertToString(_value); get => ConvertToString(_value);
set set
@ -54,20 +57,30 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
public override string Priority => public override string Priority => string.Empty;
string.Empty;
public override bool? IsAttached => public override bool? IsAttached => default;
default;
public override System.Type? DeclaringType { get; } public override Type? DeclaringType { get; }
// [MemberNotNull(nameof(_type))] // [MemberNotNull(nameof(_type))]
public override void Update() public override void Update()
{ {
var val = Property.GetValue(_target); object? value;
RaiseAndSetIfChanged(ref _value, val, nameof(Value)); Type? valueType = null;
RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType));
try
{
value = Property.GetValue(_target);
valueType = value?.GetType();
}
catch (Exception e)
{
value = e.GetBaseException();
}
RaiseAndSetIfChanged(ref _value, value, nameof(Value));
RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType));
RaisePropertyChanged(nameof(Type)); RaisePropertyChanged(nameof(Type));
} }
} }

10
src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml

@ -1,9 +1,13 @@
<Style xmlns="https://github.com/avaloniaui" <Style xmlns="https://github.com/avaloniaui"
Selector="DataValidationErrors"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Selector="DataValidationErrors"
x:CompileBindings="True"
x:DataType="DataValidationErrors">
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<ContentControl DockPanel.Dock="Right" <ContentControl x:DataType="DataValidationErrors"
DockPanel.Dock="Right"
ContentTemplate="{TemplateBinding ErrorTemplate}" ContentTemplate="{TemplateBinding ErrorTemplate}"
DataContext="{TemplateBinding Owner}" DataContext="{TemplateBinding Owner}"
Content="{Binding (DataValidationErrors.Errors)}" Content="{Binding (DataValidationErrors.Errors)}"
@ -30,7 +34,7 @@
</Style> </Style>
</Canvas.Styles> </Canvas.Styles>
<ToolTip.Tip> <ToolTip.Tip>
<ItemsControl Items="{Binding}"/> <ItemsControl x:DataType="DataValidationErrors" Items="{Binding}"/>
</ToolTip.Tip> </ToolTip.Tip>
<Path Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2" Stroke="{DynamicResource ErrorBrush}" StrokeThickness="2"/> <Path Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2" Stroke="{DynamicResource ErrorBrush}" StrokeThickness="2"/>
</Canvas> </Canvas>

3
src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml

@ -4,6 +4,7 @@
Selector="NativeMenuBar"> Selector="NativeMenuBar">
<Style.Resources> <Style.Resources>
<local:InverseBooleanValueConverter x:Key="AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter" Default="True"/> <local:InverseBooleanValueConverter x:Key="AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter" Default="True"/>
<local:IBitmapToImageConverter x:Key="AvaloniaThemesDefaultNativeMenuBarIBitmapToImageConverterr"/>
</Style.Resources> </Style.Resources>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
@ -18,6 +19,8 @@
<Setter Property="Command" Value="{Binding Command}"/> <Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/> <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
<Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/> <Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/>
<!--NativeMenuItem is IBitmap and MenuItem is Image-->
<Setter Property="Icon" Value="{Binding Icon , Converter={StaticResource AvaloniaThemesDefaultNativeMenuBarIBitmapToImageConverterr}}"/>
</Style> </Style>
</Menu.Styles> </Menu.Styles>
</Menu> </Menu>

18
src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml

@ -1,4 +1,4 @@
<Style xmlns="https://github.com/avaloniaui" <Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Selector="OverlayPopupHost"> Selector="OverlayPopupHost">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" /> <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
@ -9,13 +9,15 @@
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<!-- Do not forget to update Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent test --> <!-- Do not forget to update Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent test -->
<VisualLayerManager IsPopup="True"> <LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">
<ContentPresenter Name="PART_ContentPresenter" <VisualLayerManager IsPopup="True">
Background="{TemplateBinding Background}" <ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}" Background="{TemplateBinding Background}"
Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"/> Content="{TemplateBinding Content}"
</VisualLayerManager> Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</LayoutTransformControl>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>

22
src/Avalonia.Themes.Default/Controls/PopupRoot.xaml

@ -10,16 +10,18 @@
<Setter Property="FontStyle" Value="Normal" /> <Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Panel> <LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" /> <Panel>
<VisualLayerManager IsPopup="True"> <Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<ContentPresenter Name="PART_ContentPresenter" <VisualLayerManager IsPopup="True">
Background="{TemplateBinding Background}" <ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}" Background="{TemplateBinding Background}"
Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"/> Content="{TemplateBinding Content}"
</VisualLayerManager> Padding="{TemplateBinding Padding}"/>
</Panel> </VisualLayerManager>
</Panel>
</LayoutTransformControl>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>

28
src/Avalonia.Themes.Default/IBitmapToImageConverter.cs

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Media.Imaging;
namespace Avalonia.Themes.Default
{
internal class IBitmapToImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && value is IBitmap bm)
return new Image { Source=bm };
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

3
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@ -119,7 +119,8 @@
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}" MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="Background" PlacementTarget="Background"
IsLightDismissEnabled="True"> IsLightDismissEnabled="True"
InheritsTransform="True">
<Border x:Name="PopupBorder" <Border x:Name="PopupBorder"
Background="{DynamicResource ComboBoxDropDownBackground}" Background="{DynamicResource ComboBoxDropDownBackground}"
BorderBrush="{DynamicResource ComboBoxDropDownBorderBrush}" BorderBrush="{DynamicResource ComboBoxDropDownBorderBrush}"

7
src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml

@ -4,12 +4,13 @@
x:CompileBindings="True" x:CompileBindings="True"
Selector="NativeMenuBar"> Selector="NativeMenuBar">
<Style.Resources> <Style.Resources>
<local:InverseBooleanValueConverter x:Key="AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter" Default="True"/> <local:InverseBooleanValueConverter x:Key="AvaloniaThemesFluentNativeMenuBarInverseBooleanValueConverter" Default="True"/>
<local:IBitmapToImageConverter x:Key="AvaloniaThemesFluentNativeMenuBarIBitmapToImageConverter"/>
</Style.Resources> </Style.Resources>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Menu <Menu
IsVisible="{Binding $parent[TopLevel].(NativeMenu.IsNativeMenuExported), Converter={StaticResource AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter}}" IsVisible="{Binding $parent[TopLevel].(NativeMenu.IsNativeMenuExported), Converter={StaticResource AvaloniaThemesFluentNativeMenuBarInverseBooleanValueConverter}}"
Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}"> Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}">
<Menu.Styles> <Menu.Styles>
<Style x:CompileBindings="False" Selector="MenuItem"> <Style x:CompileBindings="False" Selector="MenuItem">
@ -19,6 +20,8 @@
<Setter Property="Command" Value="{Binding Command}"/> <Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/> <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
<Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/> <Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/>
<!--NativeMenuItem is IBitmap and MenuItem is Image-->
<Setter Property="Icon" Value="{Binding Icon , Converter={StaticResource AvaloniaThemesFluentNativeMenuBarIBitmapToImageConverter}}"/>
</Style> </Style>
</Menu.Styles> </Menu.Styles>
</Menu> </Menu>

16
src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml

@ -6,13 +6,15 @@
<Setter Property="FontStyle" Value="Normal" /> <Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<VisualLayerManager IsPopup="True"> <LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">
<ContentPresenter Name="PART_ContentPresenter" <VisualLayerManager IsPopup="True">
Background="{TemplateBinding Background}" <ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}" Background="{TemplateBinding Background}"
Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"/> Content="{TemplateBinding Content}"
</VisualLayerManager> Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</LayoutTransformControl>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>

14
src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml

@ -10,16 +10,18 @@
<Setter Property="FontStyle" Value="Normal" /> <Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Panel> <LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" /> <Panel>
<VisualLayerManager IsPopup="True"> <Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<ContentPresenter Name="PART_ContentPresenter" <VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}" ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}" Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/> Padding="{TemplateBinding Padding}"/>
</VisualLayerManager> </VisualLayerManager>
</Panel> </Panel>
</LayoutTransformControl>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</Style> </Style>

28
src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Media.Imaging;
namespace Avalonia.Themes.Fluent
{
internal class IBitmapToImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && value is IBitmap bm)
return new Image { Source=bm };
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

18
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs

@ -39,17 +39,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
targetType = setter.Property.PropertyType; targetType = setter.Property.PropertyType;
} }
// Look upwards though the ambient context for IResourceHosts and IResourceProviders // Look upwards though the ambient context for IResourceNodes
// which might be able to give us the resource. // which might be able to give us the resource.
foreach (var e in stack.Parents) foreach (var parent in stack.Parents)
{ {
object value; if (parent is IResourceNode node && node.TryGetResource(ResourceKey, out var value))
if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value))
{
return ColorToBrushConverter.Convert(value, targetType);
}
else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value))
{ {
return ColorToBrushConverter.Convert(value, targetType); return ColorToBrushConverter.Convert(value, targetType);
} }
@ -58,7 +52,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
if (provideTarget.TargetObject is IControl target && if (provideTarget.TargetObject is IControl target &&
provideTarget.TargetProperty is PropertyInfo property) provideTarget.TargetProperty is PropertyInfo property)
{ {
DelayedBinding.Add(target, property, x => GetValue(x, targetType)); // This is stored locally to avoid allocating closure in the outer scope.
var localTargetType = targetType;
var localInstance = this;
DelayedBinding.Add(target, property, x => localInstance.GetValue(x, localTargetType));
return AvaloniaProperty.UnsetValue; return AvaloniaProperty.UnsetValue;
} }

6
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@ -60,7 +60,7 @@ namespace Avalonia.Markup.Xaml.Styling
} }
} }
bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; bool IResourceNode.HasResources => Loaded?.HasResources ?? false;
IReadOnlyList<IStyle> IStyle.Children => _loaded ?? Array.Empty<IStyle>(); IReadOnlyList<IStyle> IStyle.Children => _loaded ?? Array.Empty<IStyle>();
@ -86,9 +86,9 @@ namespace Avalonia.Markup.Xaml.Styling
public bool TryGetResource(object key, out object? value) public bool TryGetResource(object key, out object? value)
{ {
if (!_isLoading && Loaded is IResourceProvider p) if (!_isLoading)
{ {
return p.TryGetResource(key, out value); return Loaded.TryGetResource(key, out value);
} }
value = null; value = null;

1
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -126,6 +126,7 @@ namespace Avalonia.Skia
SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas; SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas;
SKSurface ISkiaDrawingContextImpl.SkSurface => Surface; SKSurface ISkiaDrawingContextImpl.SkSurface => Surface;
GRContext ISkiaDrawingContextImpl.GrContext => _grContext; GRContext ISkiaDrawingContextImpl.GrContext => _grContext;
double ISkiaDrawingContextImpl.CurrentOpacity => _currentOpacity;
/// <inheritdoc /> /// <inheritdoc />
public void Clear(Color color) public void Clear(Color color)

1
src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs

@ -8,5 +8,6 @@ namespace Avalonia.Skia
SKCanvas SkCanvas { get; } SKCanvas SkCanvas { get; }
GRContext GrContext { get; } GRContext GrContext { get; }
SKSurface SkSurface { get; } SKSurface SkSurface { get; }
double CurrentOpacity { get; }
} }
} }

7
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -27,7 +27,7 @@ namespace Avalonia.Skia
buffer.GuessSegmentProperties(); buffer.GuessSegmentProperties();
buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; buffer.Direction = Direction.LeftToRight; //Always shape LeftToRight
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
@ -35,11 +35,6 @@ namespace Avalonia.Skia
font.Shape(buffer); font.Shape(buffer);
if (buffer.Direction == Direction.RightToLeft)
{
buffer.Reverse();
}
font.GetScale(out var scaleX, out _); font.GetScale(out var scaleX, out _);
var textScale = fontRenderingEmSize / scaleX; var textScale = fontRenderingEmSize / scaleX;

33
src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform; using Avalonia.Platform;
@ -11,8 +10,7 @@ using GlyphInfo = HarfBuzzSharp.GlyphInfo;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
internal class TextShaperImpl : ITextShaperImpl
internal class TextShaperImpl : ITextShaperImpl
{ {
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options) public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options)
{ {
@ -23,25 +21,20 @@ internal class TextShaperImpl : ITextShaperImpl
using (var buffer = new Buffer()) using (var buffer = new Buffer())
{ {
buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length);
MergeBreakPair(buffer); MergeBreakPair(buffer);
buffer.GuessSegmentProperties(); buffer.GuessSegmentProperties();
buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; buffer.Direction = Direction.LeftToRight; //Always shape LeftToRight
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
var font = ((GlyphTypefaceImpl)typeface.PlatformImpl).Font; var font = ((GlyphTypefaceImpl)typeface.PlatformImpl).Font;
font.Shape(buffer); font.Shape(buffer);
if (buffer.Direction == Direction.RightToLeft)
{
buffer.Reverse();
}
font.GetScale(out var scaleX, out _); font.GetScale(out var scaleX, out _);
var textScale = fontRenderingEmSize / scaleX; var textScale = fontRenderingEmSize / scaleX;
@ -60,13 +53,13 @@ internal class TextShaperImpl : ITextShaperImpl
var glyphIndex = (ushort)sourceInfo.Codepoint; var glyphIndex = (ushort)sourceInfo.Codepoint;
var glyphCluster = (int)sourceInfo.Cluster; var glyphCluster = (int)(sourceInfo.Cluster);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
if (glyphIndex == 0 && text[glyphCluster] == '\t') if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
{ {
glyphIndex = typeface.GetGlyph(' '); glyphIndex = typeface.GetGlyph(' ');
@ -75,9 +68,7 @@ internal class TextShaperImpl : ITextShaperImpl
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
} }
var targetInfo = var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance,
glyphOffset);
shapedBuffer[i] = targetInfo; shapedBuffer[i] = targetInfo;
} }
@ -91,7 +82,7 @@ internal class TextShaperImpl : ITextShaperImpl
var length = buffer.Length; var length = buffer.Length;
var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphInfos = buffer.GetGlyphInfoSpan();
var second = glyphInfos[length - 1]; var second = glyphInfos[length - 1];
if (!new Codepoint((int)second.Codepoint).IsBreakChar) if (!new Codepoint((int)second.Codepoint).IsBreakChar)
@ -102,7 +93,7 @@ internal class TextShaperImpl : ITextShaperImpl
if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n')
{ {
var first = glyphInfos[length - 2]; var first = glyphInfos[length - 2];
first.Codepoint = '\u200C'; first.Codepoint = '\u200C';
second.Codepoint = '\u200C'; second.Codepoint = '\u200C';
second.Cluster = first.Cluster; second.Cluster = first.Cluster;
@ -113,7 +104,7 @@ internal class TextShaperImpl : ITextShaperImpl
{ {
*p = first; *p = first;
} }
fixed (GlyphInfo* p = &glyphInfos[length - 1]) fixed (GlyphInfo* p = &glyphInfos[length - 1])
{ {
*p = second; *p = second;
@ -148,7 +139,7 @@ internal class TextShaperImpl : ITextShaperImpl
private static double GetGlyphAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale) private static double GetGlyphAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale)
{ {
// Depends on direction of layout // Depends on direction of layout
// advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; // glyphPositions[index].YAdvance * textScale;
return glyphPositions[index].XAdvance * textScale; return glyphPositions[index].XAdvance * textScale;
} }
} }

4
src/iOS/Avalonia.iOS/TouchHandler.cs

@ -41,7 +41,7 @@ namespace Avalonia.iOS
_ => RawPointerEventType.TouchUpdate _ => RawPointerEventType.TouchUpdate
}, pt, RawInputModifiers.None, id); }, pt, RawInputModifiers.None, id);
_device.ProcessRawEvent(ev); _tl.Input?.Invoke(ev);
if (t.Phase == UITouchPhase.Cancelled || t.Phase == UITouchPhase.Ended) if (t.Phase == UITouchPhase.Cancelled || t.Phase == UITouchPhase.Ended)
_knownTouches.Remove(t); _knownTouches.Remove(t);
@ -49,4 +49,4 @@ namespace Avalonia.iOS
} }
} }
} }

8
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@ -78,9 +78,13 @@ namespace Avalonia.Controls.UnitTests.Primitives
var templatedChild = ((Visual)target.Host).GetVisualChildren().Single(); var templatedChild = ((Visual)target.Host).GetVisualChildren().Single();
Assert.IsType<Panel>(templatedChild); Assert.IsType<LayoutTransformControl>(templatedChild);
var visualLayerManager = templatedChild.GetVisualChildren().Skip(1).Single(); var panel = templatedChild.GetVisualChildren().Single();
Assert.IsType<Panel>(panel);
var visualLayerManager = panel.GetVisualChildren().Skip(1).Single();
Assert.IsType<VisualLayerManager>(visualLayerManager); Assert.IsType<VisualLayerManager>(visualLayerManager);

4
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -325,6 +325,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal( Assert.Equal(
new[] new[]
{ {
"LayoutTransformControl",
"VisualLayerManager", "VisualLayerManager",
"ContentPresenter", "ContentPresenter",
"ContentPresenter", "ContentPresenter",
@ -337,6 +338,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal( Assert.Equal(
new[] new[]
{ {
"LayoutTransformControl",
"Panel", "Panel",
"Border", "Border",
"VisualLayerManager", "VisualLayerManager",
@ -356,6 +358,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal( Assert.Equal(
new object[] new object[]
{ {
popupRoot,
popupRoot, popupRoot,
popupRoot, popupRoot,
target, target,
@ -372,6 +375,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
popupRoot, popupRoot,
popupRoot, popupRoot,
popupRoot, popupRoot,
popupRoot,
target, target,
null, null,
}, },

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

Loading…
Cancel
Save