Browse Source

Merge branch 'master' into fixes/7147-combobox-transform

pull/8042/head
Steven Kirk 4 years ago
committed by GitHub
parent
commit
61b2176c3b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  2. 15
      native/Avalonia.Native/src/OSX/INSWindowHolder.h
  3. 18
      native/Avalonia.Native/src/OSX/IWindowStateChanged.h
  4. 23
      native/Avalonia.Native/src/OSX/ResizeScope.h
  5. 17
      native/Avalonia.Native/src/OSX/ResizeScope.mm
  6. 1
      native/Avalonia.Native/src/OSX/SystemDialogs.mm
  7. 119
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  8. 505
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  9. 97
      native/Avalonia.Native/src/OSX/WindowImpl.h
  10. 540
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  11. 12
      native/Avalonia.Native/src/OSX/app.mm
  12. 1
      native/Avalonia.Native/src/OSX/automation.h
  13. 5
      native/Avalonia.Native/src/OSX/automation.mm
  14. 3
      native/Avalonia.Native/src/OSX/common.h
  15. 1
      native/Avalonia.Native/src/OSX/cursor.mm
  16. 6
      native/Avalonia.Native/src/OSX/main.mm
  17. 1
      native/Avalonia.Native/src/OSX/menu.h
  18. 7
      native/Avalonia.Native/src/OSX/menu.mm
  19. 4
      native/Avalonia.Native/src/OSX/rendertarget.mm
  20. 47
      native/Avalonia.Native/src/OSX/window.h
  21. 1358
      native/Avalonia.Native/src/OSX/window.mm
  22. 4
      samples/ControlCatalog/App.xaml.cs
  23. 86
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  24. 11
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  25. 79
      src/Avalonia.Base/Media/GlyphRun.cs
  26. 4
      src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
  27. 65
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  28. 119
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  29. 179
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  30. 5
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  31. 2
      src/Avalonia.Base/Styling/IStyle.cs
  32. 2
      src/Avalonia.Base/Styling/Styles.cs
  33. 2
      src/Avalonia.Base/Utilities/WeakHashList.cs
  34. 28
      src/Avalonia.Controls.ColorPicker/ColorComponent.cs
  35. 18
      src/Avalonia.Controls.ColorPicker/ColorModel.cs
  36. 50
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
  37. 130
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
  38. 146
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
  39. 399
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
  40. 414
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs
  41. 151
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  42. 138
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  43. 0
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs
  44. 0
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs
  45. 116
      src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs
  46. 68
      src/Avalonia.Controls.ColorPicker/Converters/ColorToDisplayNameConverter.cs
  47. 82
      src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
  48. 51
      src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs
  49. 50
      src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs
  50. 58
      src/Avalonia.Controls.ColorPicker/Converters/ToColorConverter.cs
  51. 50
      src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs
  52. 142
      src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs
  53. 629
      src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs
  54. 0
      src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs
  55. 0
      src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs
  56. 0
      src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs
  57. 0
      src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs
  58. 22
      src/Avalonia.Controls.ColorPicker/HsvComponent.cs
  59. 42
      src/Avalonia.Controls.ColorPicker/RgbComponent.cs
  60. 86
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml
  61. 194
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml
  62. 22
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml
  63. 28
      src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml
  64. 86
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
  65. 194
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml
  66. 25
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml
  67. 28
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml
  68. 15
      src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs
  69. 11
      src/Avalonia.Controls/Documents/IInlineHost.cs
  70. 9
      src/Avalonia.Controls/Documents/Inline.cs
  71. 46
      src/Avalonia.Controls/Documents/InlineCollection.cs
  72. 125
      src/Avalonia.Controls/Documents/InlineUIContainer.cs
  73. 20
      src/Avalonia.Controls/Documents/LineBreak.cs
  74. 19
      src/Avalonia.Controls/Documents/Run.cs
  75. 46
      src/Avalonia.Controls/Documents/Span.cs
  76. 15
      src/Avalonia.Controls/Documents/TextElement.cs
  77. 2
      src/Avalonia.Controls/ItemsControl.cs
  78. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  79. 27
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  80. 148
      src/Avalonia.Controls/TextBlock.cs
  81. 2
      src/Avalonia.Controls/Viewbox.cs
  82. 64
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  83. 41
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  84. 10
      src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml
  85. 3
      src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml
  86. 28
      src/Avalonia.Themes.Default/IBitmapToImageConverter.cs
  87. 7
      src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
  88. 28
      src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs
  89. 18
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
  90. 6
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  91. 10
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  92. 33
      src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
  93. 75
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  94. 15
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  95. 45
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  96. 3
      tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
  97. 119
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  98. BIN
      tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png
  99. BIN
      tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

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

@ -7,6 +7,14 @@
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 */; };
18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839171D898F9BFC1373631A /* ResizeScope.h */; };
18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */ = {isa = PBXBuildFile; fileRef = 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */; };
1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 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 +43,14 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IWindowStateChanged.h; sourceTree = "<group>"; };
183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowBaseImpl.h; sourceTree = "<group>"; };
18391676ECF0E983F4964357 /* WindowBaseImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowBaseImpl.mm; sourceTree = "<group>"; };
1839171D898F9BFC1373631A /* ResizeScope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResizeScope.h; sourceTree = "<group>"; };
183919BF108EB72A029F7671 /* WindowImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowImpl.mm; sourceTree = "<group>"; };
18391BBB7782C296D424071F /* INSWindowHolder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = INSWindowHolder.h; sourceTree = "<group>"; };
18391CD090AA776E7E841AC9 /* WindowImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowImpl.h; sourceTree = "<group>"; };
18391E45702740FE9DD69695 /* ResizeScope.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ResizeScope.mm; sourceTree = "<group>"; };
1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = "<group>"; }; 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 +146,14 @@
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 */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -150,6 +174,11 @@
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 */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -229,6 +258,9 @@
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 */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

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

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

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

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

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

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

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

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

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

@ -1,5 +1,6 @@
#include "common.h" #include "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>
{ {

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

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

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

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

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

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

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

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

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

@ -82,18 +82,6 @@ ComPtr<IAvnApplicationEvents> _events;
_isHandlingSendEvent = oldHandling; _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

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

@ -1,7 +1,8 @@
#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"
@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

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

@ -1,6 +1,10 @@
#ifndef window_h #ifndef window_h
#define window_h #define window_h
#import "avalonia-native.h"
@class AvnMenu;
class WindowBaseImpl; class WindowBaseImpl;
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination> @interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
@ -9,7 +13,7 @@ class WindowBaseImpl;
-(AvnPoint) translateLocalPoint:(AvnPoint)pt; -(AvnPoint) translateLocalPoint:(AvnPoint)pt;
-(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; -(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose;
-(void) onClosed; -(void) onClosed;
-(AvnPixelSize) getPixelSize;
-(AvnPlatformResizeReason) getResizeReason; -(AvnPlatformResizeReason) getResizeReason;
-(void) setResizeReason:(AvnPlatformResizeReason)reason; -(void) setResizeReason:(AvnPlatformResizeReason)reason;
+ (AvnPoint)toAvnPoint:(CGPoint)p; + (AvnPoint)toAvnPoint:(CGPoint)p;
@ -19,12 +23,11 @@ class WindowBaseImpl;
-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; -(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content;
-(void) ShowTitleBar: (bool) show; -(void) ShowTitleBar: (bool) show;
-(void) SetTitleBarHeightHint: (double) height; -(void) SetTitleBarHeightHint: (double) height;
-(void) SetContent: (NSView* _Nonnull) content;
-(void) ShowBlur: (bool) show; -(void) ShowBlur: (bool) show;
@end @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 +36,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 */

1358
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>

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;

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

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

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

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

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

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

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

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

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

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

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

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

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

11
src/Avalonia.Controls/Documents/IInlineHost.cs

@ -0,0 +1,11 @@
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Documents
{
internal interface IInlineHost : ILogical
{
void AddVisualChild(IControl child);
void Invalidate();
}
}

9
src/Avalonia.Controls/Documents/Inline.cs

@ -2,9 +2,8 @@ using System.Collections.Generic;
using System.Text; using System.Text;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Controls.Documents namespace Avalonia.Controls.Documents
{ {
/// <summary> /// <summary>
/// Inline element. /// Inline element.
@ -45,9 +44,9 @@ namespace Avalonia.Controls.Documents
set { SetValue(BaselineAlignmentProperty, value); } set { SetValue(BaselineAlignmentProperty, value); }
} }
internal abstract int BuildRun(StringBuilder stringBuilder, IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex); internal abstract void BuildTextRun(IList<TextRun> textRuns);
internal abstract int AppendText(StringBuilder stringBuilder); internal abstract void AppendText(StringBuilder stringBuilder);
protected TextRunProperties CreateTextRunProperties() protected TextRunProperties CreateTextRunProperties()
{ {
@ -63,7 +62,7 @@ namespace Avalonia.Controls.Documents
{ {
case nameof(TextDecorations): case nameof(TextDecorations):
case nameof(BaselineAlignment): case nameof(BaselineAlignment):
Invalidate(); InlineHost?.Invalidate();
break; break;
} }
} }

46
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -12,29 +12,37 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection] [WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList<Inline> public class InlineCollection : AvaloniaList<Inline>
{ {
private readonly IInlineHost? _host;
private string? _text = string.Empty; private string? _text = string.Empty;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class. /// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary> /// </summary>
public InlineCollection(ILogical parent) : base(0) public InlineCollection(ILogical parent) : this(parent, null) { }
/// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary>
internal InlineCollection(ILogical parent, IInlineHost? host = null) : base(0)
{ {
_host = host;
ResetBehavior = ResetBehavior.Remove; ResetBehavior = ResetBehavior.Remove;
this.ForEachItem( this.ForEachItem(
x => x =>
{ {
((ISetLogicalParent)x).SetParent(parent); ((ISetLogicalParent)x).SetParent(parent);
x.Invalidated += Invalidate; x.InlineHost = host;
Invalidate(); host?.Invalidate();
}, },
x => x =>
{ {
((ISetLogicalParent)x).SetParent(null); ((ISetLogicalParent)x).SetParent(null);
x.Invalidated -= Invalidate; x.InlineHost = host;
Invalidate(); host?.Invalidate();
}, },
() => throw new NotSupportedException()); () => throw new NotSupportedException());
} }
public bool HasComplexContent => Count > 0; public bool HasComplexContent => Count > 0;
@ -96,12 +104,22 @@ namespace Avalonia.Controls.Documents
} }
} }
public void Add(IControl child)
{
var implicitRun = new InlineUIContainer(child);
Add(implicitRun);
}
public override void Add(Inline item) public override void Add(Inline item)
{ {
if (!HasComplexContent) if (!HasComplexContent)
{ {
base.Add(new Run(_text)); if (!string.IsNullOrEmpty(_text))
{
base.Add(new Run(_text));
}
_text = string.Empty; _text = string.Empty;
} }
@ -112,11 +130,19 @@ namespace Avalonia.Controls.Documents
/// Raised when an inline in the collection changes. /// Raised when an inline in the collection changes.
/// </summary> /// </summary>
public event EventHandler? Invalidated; public event EventHandler? Invalidated;
/// <summary> /// <summary>
/// Raises the <see cref="Invalidated"/> event. /// Raises the <see cref="Invalidated"/> event.
/// </summary> /// </summary>
protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty); protected void Invalidate()
{
if(_host != null)
{
_host.Invalidate();
}
Invalidated?.Invoke(this, EventArgs.Empty);
}
private void Invalidate(object? sender, EventArgs e) => Invalidate(); private void Invalidate(object? sender, EventArgs e) => Invalidate();
} }

125
src/Avalonia.Controls/Documents/InlineUIContainer.cs

@ -0,0 +1,125 @@
using System.Collections.Generic;
using System.Text;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls.Documents
{
/// <summary>
/// InlineUIContainer - a wrapper for embedded UIElements in text
/// flow content inline collections
/// </summary>
public class InlineUIContainer : Inline
{
/// <summary>
/// Defines the <see cref="Child"/> property.
/// </summary>
public static readonly StyledProperty<IControl> ChildProperty =
AvaloniaProperty.Register<InlineUIContainer, IControl>(nameof(Child));
static InlineUIContainer()
{
BaselineAlignmentProperty.OverrideDefaultValue<InlineUIContainer>(BaselineAlignment.Top);
}
/// <summary>
/// Initializes a new instance of InlineUIContainer element.
/// </summary>
/// <remarks>
/// The purpose of this element is to be a wrapper for UIElements
/// when they are embedded into text flow - as items of
/// InlineCollections.
/// </remarks>
public InlineUIContainer()
{
}
/// <summary>
/// Initializes an InlineBox specifying its child UIElement
/// </summary>
/// <param name="child">
/// UIElement set as a child of this inline item
/// </param>
public InlineUIContainer(IControl child)
{
Child = child;
}
/// <summary>
/// The content spanned by this TextElement.
/// </summary>
[Content]
public IControl Child
{
get => GetValue(ChildProperty);
set => SetValue(ChildProperty, value);
}
internal override void BuildTextRun(IList<TextRun> textRuns)
{
if(InlineHost == null)
{
return;
}
((ISetLogicalParent)Child).SetParent(InlineHost);
InlineHost.AddVisualChild(Child);
textRuns.Add(new InlineRun(Child, CreateTextRunProperties()));
}
internal override void AppendText(StringBuilder stringBuilder)
{
}
private class InlineRun : DrawableTextRun
{
public InlineRun(IControl control, TextRunProperties properties)
{
Control = control;
Properties = properties;
}
public IControl Control { get; }
public override TextRunProperties? Properties { get; }
public override Size Size
{
get
{
if (!Control.IsMeasureValid)
{
Control.Measure(Size.Infinity);
}
return Control.DesiredSize;
}
}
public override double Baseline
{
get
{
double baseline = Size.Height;
double baselineOffsetValue = Control.GetValue<double>(TextBlock.BaselineOffsetProperty);
if (!MathUtilities.IsZero(baselineOffsetValue))
{
baseline = baselineOffsetValue;
}
return -baseline;
}
}
public override void Draw(DrawingContext drawingContext, Point origin)
{
Control.Arrange(new Rect(origin, Size));
}
}
}
}

20
src/Avalonia.Controls/Documents/LineBreak.cs

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Avalonia.LogicalTree;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls.Documents namespace Avalonia.Controls.Documents
{ {
@ -20,24 +20,14 @@ namespace Avalonia.Controls.Documents
{ {
} }
internal override int BuildRun(StringBuilder stringBuilder, internal override void BuildTextRun(IList<TextRun> textRuns)
IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
{ {
var length = AppendText(stringBuilder); textRuns.Add(new TextEndOfLine());
textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length,
CreateTextRunProperties()));
return length;
} }
internal override int AppendText(StringBuilder stringBuilder) internal override void AppendText(StringBuilder stringBuilder)
{ {
var text = Environment.NewLine; stringBuilder.Append(Environment.NewLine);
stringBuilder.Append(text);
return text.Length;
} }
} }
} }

19
src/Avalonia.Controls/Documents/Run.cs

@ -4,7 +4,6 @@ using System.Text;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls.Documents namespace Avalonia.Controls.Documents
{ {
@ -51,24 +50,22 @@ namespace Avalonia.Controls.Documents
set { SetValue (TextProperty, value); } set { SetValue (TextProperty, value); }
} }
internal override int BuildRun(StringBuilder stringBuilder, internal override void BuildTextRun(IList<TextRun> textRuns)
IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
{ {
var length = AppendText(stringBuilder); var text = (Text ?? "").AsMemory();
textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length, var textRunProperties = CreateTextRunProperties();
CreateTextRunProperties()));
return length; var textCharacters = new TextCharacters(text, textRunProperties);
textRuns.Add(textCharacters);
} }
internal override int AppendText(StringBuilder stringBuilder) internal override void AppendText(StringBuilder stringBuilder)
{ {
var text = Text ?? ""; var text = Text ?? "";
stringBuilder.Append(text); stringBuilder.Append(text);
return text.Length;
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@ -78,7 +75,7 @@ namespace Avalonia.Controls.Documents
switch (change.Property.Name) switch (change.Property.Name)
{ {
case nameof(Text): case nameof(Text):
Invalidate(); InlineHost?.Invalidate();
break; break;
} }
} }

46
src/Avalonia.Controls/Documents/Span.cs

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls.Documents namespace Avalonia.Controls.Documents
{ {
@ -25,8 +25,7 @@ namespace Avalonia.Controls.Documents
public Span() public Span()
{ {
Inlines = new InlineCollection(this); Inlines = new InlineCollection(this);
Inlines.Invalidated += (s, e) => InlineHost?.Invalidate();
Inlines.Invalidated += (s, e) => Invalidate();
} }
/// <summary> /// <summary>
@ -35,61 +34,42 @@ namespace Avalonia.Controls.Documents
[Content] [Content]
public InlineCollection Inlines { get; } public InlineCollection Inlines { get; }
internal override int BuildRun(StringBuilder stringBuilder, IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex) internal override void BuildTextRun(IList<TextRun> textRuns)
{ {
var length = 0;
if (Inlines.HasComplexContent) if (Inlines.HasComplexContent)
{ {
foreach (var inline in Inlines) foreach (var inline in Inlines)
{ {
var inlineLength = inline.BuildRun(stringBuilder, textStyleOverrides, firstCharacterIndex); inline.BuildTextRun(textRuns);
firstCharacterIndex += inlineLength;
length += inlineLength;
} }
} }
else else
{ {
if (Inlines.Text == null) if (Inlines.Text is string text)
{ {
return length; var textRunProperties = CreateTextRunProperties();
}
stringBuilder.Append(Inlines.Text);
length = Inlines.Text.Length; var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties);
textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length, textRuns.Add(textCharacters);
CreateTextRunProperties())); }
} }
return length;
} }
internal override int AppendText(StringBuilder stringBuilder) internal override void AppendText(StringBuilder stringBuilder)
{ {
if (Inlines.HasComplexContent) if (Inlines.HasComplexContent)
{ {
var length = 0;
foreach (var inline in Inlines) foreach (var inline in Inlines)
{ {
length += inline.AppendText(stringBuilder); inline.AppendText(stringBuilder);
} }
return length;
} }
if (Inlines.Text == null) if (Inlines.Text is string text)
{ {
return 0; stringBuilder.Append(text);
} }
stringBuilder.Append(Inlines.Text);
return Inlines.Text.Length;
} }
} }
} }

15
src/Avalonia.Controls/Documents/TextElement.cs

@ -1,5 +1,4 @@
using System; using Avalonia.Media;
using Avalonia.Media;
namespace Avalonia.Controls.Documents namespace Avalonia.Controls.Documents
{ {
@ -251,10 +250,7 @@ namespace Avalonia.Controls.Documents
control.SetValue(ForegroundProperty, value); control.SetValue(ForegroundProperty, value);
} }
/// <summary> internal IInlineHost? InlineHost { get; set; }
/// Raised when the visual representation of the text element changes.
/// </summary>
public event EventHandler? Invalidated;
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
@ -269,14 +265,9 @@ namespace Avalonia.Controls.Documents
case nameof(FontWeight): case nameof(FontWeight):
case nameof(FontStretch): case nameof(FontStretch):
case nameof(Foreground): case nameof(Foreground):
Invalidate(); InlineHost?.Invalidate();
break; break;
} }
} }
/// <summary>
/// Raises the <see cref="Invalidate"/> event.
/// </summary>
protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty);
} }
} }

2
src/Avalonia.Controls/ItemsControl.cs

@ -518,7 +518,7 @@ namespace Avalonia.Controls
} }
c = result; c = result;
} while (c != null && c != from); } while (c != null && c != from && direction != NavigationDirection.First && direction != NavigationDirection.Last);
return null; return null;
} }

2
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -417,7 +417,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void MenuOpened(object? sender, RoutedEventArgs e) protected internal virtual void MenuOpened(object? sender, RoutedEventArgs e)
{ {
if (e.Source == Menu) if (e.Source is Menu)
{ {
Menu?.MoveSelection(NavigationDirection.First, true); Menu?.MoveSelection(NavigationDirection.First, true);
} }

27
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;
@ -662,7 +673,7 @@ namespace Avalonia.Controls.Presenters
caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (textLine.NewLineLength > 0 && caretIndex == textLine.FirstTextSourceIndex + textLine.Length) if (textLine.TrailingWhitespaceLength > 0 && caretIndex == textLine.FirstTextSourceIndex + textLine.Length)
{ {
characterHit = new CharacterHit(caretIndex); characterHit = new CharacterHit(caretIndex);
} }

148
src/Avalonia.Controls/TextBlock.cs

@ -14,7 +14,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control that displays a block of text. /// A control that displays a block of text.
/// </summary> /// </summary>
public class TextBlock : Control public class TextBlock : Control, IInlineHost
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Background"/> property. /// Defines the <see cref="Background"/> property.
@ -155,9 +155,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public TextBlock() public TextBlock()
{ {
Inlines = new InlineCollection(this); Inlines = new InlineCollection(this, this);
Inlines.Invalidated += InlinesChanged;
} }
/// <summary> /// <summary>
@ -211,7 +209,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Gets or sets the inlines. /// Gets the inlines.
/// </summary> /// </summary>
[Content] [Content]
public InlineCollection Inlines { get; } public InlineCollection Inlines { get; }
@ -552,38 +550,41 @@ namespace Avalonia.Controls
/// <returns>A <see cref="TextLayout"/> object.</returns> /// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout CreateTextLayout(Size constraint, string? text) protected virtual TextLayout CreateTextLayout(Size constraint, string? text)
{ {
List<ValueSpan<TextRunProperties>>? textStyleOverrides = null; var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
FontSize,
TextDecorations,
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
if (Inlines.HasComplexContent) if (Inlines.HasComplexContent)
{ {
textStyleOverrides = new List<ValueSpan<TextRunProperties>>(Inlines.Count); var textRuns = new List<TextRun>();
var textPosition = 0;
var stringBuilder = new StringBuilder();
foreach (var inline in Inlines) foreach (var inline in Inlines)
{ {
textPosition += inline.BuildRun(stringBuilder, textStyleOverrides, textPosition); inline.BuildTextRun(textRuns);
} }
text = stringBuilder.ToString(); textSource = new InlinesTextSource(textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
} }
return new TextLayout( return new TextLayout(
text ?? string.Empty, textSource,
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), paragraphProperties,
FontSize,
Foreground ?? Brushes.Transparent,
TextAlignment,
TextWrapping,
TextTrimming, TextTrimming,
TextDecorations,
FlowDirection,
constraint.Width, constraint.Width,
constraint.Height, constraint.Height,
maxLines: MaxLines, maxLines: MaxLines,
lineHeight: LineHeight, lineHeight: LineHeight);
textStyleOverrides: textStyleOverrides);
} }
/// <summary> /// <summary>
@ -592,7 +593,7 @@ namespace Avalonia.Controls
protected void InvalidateTextLayout() protected void InvalidateTextLayout()
{ {
_textLayout = null; _textLayout = null;
InvalidateMeasure(); InvalidateMeasure();
} }
@ -603,28 +604,35 @@ 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);
_textLayout = null; _textLayout = null;
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;
} }
_constraint = new Size(finalSize.Width, Math.Ceiling(finalSize.Height)); _constraint = new Size(finalSize.Width, double.PositiveInfinity);
_textLayout = null; _textLayout = null;
return finalSize; return finalSize;
@ -660,8 +668,6 @@ namespace Avalonia.Controls
case nameof (Padding): case nameof (Padding):
case nameof (LineHeight): case nameof (LineHeight):
case nameof (MaxLines): case nameof (MaxLines):
case nameof (InlinesProperty):
case nameof (Text): case nameof (Text):
case nameof (TextDecorations): case nameof (TextDecorations):
@ -673,9 +679,83 @@ namespace Avalonia.Controls
} }
} }
private void InlinesChanged(object? sender, EventArgs e) private void InlinesChanged(object? sender, EventArgs e)
{
InvalidateTextLayout();
}
void IInlineHost.AddVisualChild(IControl child)
{
if (child.VisualParent == null)
{
VisualChildren.Add(child);
}
}
void IInlineHost.Invalidate()
{ {
InvalidateTextLayout(); InvalidateTextLayout();
} }
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
{
_textRuns = textRuns;
}
public TextRun? GetTextRun(int textSourceIndex)
{
var currentPosition = 0;
foreach (var textRun in _textRuns)
{
if(textRun.TextSourceLength == 0)
{
continue;
}
if(currentPosition >= textSourceIndex)
{
return textRun;
}
currentPosition += textRun.TextSourceLength;
}
return null;
}
}
private readonly struct SimpleTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly TextRunProperties _defaultProperties;
public SimpleTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties)
{
_text = text;
_defaultProperties = defaultProperties;
}
public TextRun? GetTextRun(int textSourceIndex)
{
if (textSourceIndex > _text.Length)
{
return null;
}
var runText = _text.Skip(textSourceIndex);
if (runText.IsEmpty)
{
return null;
}
return new TextCharacters(runText, _defaultProperties);
}
}
} }
} }

2
src/Avalonia.Controls/Viewbox.cs

@ -14,7 +14,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="Stretch"/> property. /// Defines the <see cref="Stretch"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Stretch> StretchProperty = public static readonly StyledProperty<Stretch> StretchProperty =
AvaloniaProperty.Register<Image, Stretch>(nameof(Stretch), Stretch.Uniform); AvaloniaProperty.Register<Viewbox, Stretch>(nameof(Stretch), Stretch.Uniform);
/// <summary> /// <summary>
/// Defines the <see cref="StretchDirection"/> property. /// Defines the <see cref="StretchDirection"/> property.

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>

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

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>

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;

10
src/Skia/Avalonia.Skia/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;
@ -28,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);
@ -36,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;
@ -59,7 +53,7 @@ namespace Avalonia.Skia
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);

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

75
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -914,9 +914,9 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip,
MonitorEnumDelegate lpfnEnum, IntPtr dwData); MonitorEnumDelegate lpfnEnum, IntPtr dwData);
public delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData); public delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData);
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetDC(IntPtr hWnd); public static extern IntPtr GetDC(IntPtr hWnd);
@ -1007,7 +1007,7 @@ namespace Avalonia.Win32.Interop
public static uint GetWindowLong(IntPtr hWnd, int nIndex) public static uint GetWindowLong(IntPtr hWnd, int nIndex)
{ {
if(IntPtr.Size == 4) if (IntPtr.Size == 4)
{ {
return GetWindowLong32b(hWnd, nIndex); return GetWindowLong32b(hWnd, nIndex);
} }
@ -1034,7 +1034,7 @@ namespace Avalonia.Win32.Interop
return (uint)SetWindowLong64b(hWnd, nIndex, new IntPtr((uint)value)).ToInt32(); return (uint)SetWindowLong64b(hWnd, nIndex, new IntPtr((uint)value)).ToInt32();
} }
} }
public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr handle) public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr handle)
{ {
if (IntPtr.Size == 4) if (IntPtr.Size == 4)
@ -1068,14 +1068,14 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern bool InvalidateRect(IntPtr hWnd, RECT* lpRect, bool bErase); public static extern bool InvalidateRect(IntPtr hWnd, RECT* lpRect, bool bErase);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern bool ValidateRect(IntPtr hWnd, IntPtr lpRect); public static extern bool ValidateRect(IntPtr hWnd, IntPtr lpRect);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern bool IsWindow(IntPtr hWnd); public static extern bool IsWindow(IntPtr hWnd);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern bool IsWindowEnabled(IntPtr hWnd); public static extern bool IsWindowEnabled(IntPtr hWnd);
@ -1102,22 +1102,25 @@ namespace Avalonia.Win32.Interop
[DllImport("user32")] [DllImport("user32")]
public static extern IntPtr GetMessageExtraInfo(); public static extern IntPtr GetMessageExtraInfo();
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "RegisterClassExW")] [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "RegisterClassExW")]
public static extern ushort RegisterClassEx(ref WNDCLASSEX lpwcx); public static extern ushort RegisterClassEx(ref WNDCLASSEX lpwcx);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern void RegisterTouchWindow(IntPtr hWnd, int flags); public static extern void RegisterTouchWindow(IntPtr hWnd, int flags);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern bool ReleaseCapture(); public static extern bool ReleaseCapture();
[DllImport("user32.dll", SetLastError = true)]
public static extern uint RegisterWindowMessage(string lpString);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint);
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetActiveWindow(); public static extern IntPtr GetActiveWindow();
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetActiveWindow(IntPtr hWnd); public static extern IntPtr SetActiveWindow(IntPtr hWnd);
@ -1304,7 +1307,7 @@ namespace Avalonia.Win32.Interop
[DllImport("kernel32.dll", SetLastError = true)] [DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadLibrary(string fileName); public static extern IntPtr LoadLibrary(string fileName);
[DllImport("kernel32.dll", SetLastError = true)] [DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadLibraryEx(string fileName, IntPtr hFile, int flags); public static extern IntPtr LoadLibraryEx(string fileName, IntPtr hFile, int flags);
@ -1348,7 +1351,7 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern IntPtr MonitorFromWindow(IntPtr hwnd, MONITOR dwFlags); public static extern IntPtr MonitorFromWindow(IntPtr hwnd, MONITOR dwFlags);
[DllImport("user32", EntryPoint = "GetMonitorInfoW", ExactSpelling = true, CharSet = CharSet.Unicode)] [DllImport("user32", EntryPoint = "GetMonitorInfoW", ExactSpelling = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetMonitorInfo([In] IntPtr hMonitor, ref MONITORINFO lpmi); public static extern bool GetMonitorInfo([In] IntPtr hMonitor, ref MONITORINFO lpmi);
@ -1356,14 +1359,14 @@ namespace Avalonia.Win32.Interop
[DllImport("user32")] [DllImport("user32")]
public static extern unsafe bool GetTouchInputInfo( public static extern unsafe bool GetTouchInputInfo(
IntPtr hTouchInput, IntPtr hTouchInput,
uint cInputs, uint cInputs,
TOUCHINPUT* pInputs, TOUCHINPUT* pInputs,
int cbSize int cbSize
); );
[DllImport("user32")] [DllImport("user32")]
public static extern bool CloseTouchInputHandle(IntPtr hTouchInput); public static extern bool CloseTouchInputHandle(IntPtr hTouchInput);
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")] [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")]
public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
@ -1372,7 +1375,7 @@ namespace Avalonia.Win32.Interop
public static extern int SetDIBitsToDevice(IntPtr hdc, int XDest, int YDest, uint public static extern int SetDIBitsToDevice(IntPtr hdc, int XDest, int YDest, uint
dwWidth, uint dwHeight, int XSrc, int YSrc, uint uStartScan, uint cScanLines, dwWidth, uint dwHeight, int XSrc, int YSrc, uint uStartScan, uint cScanLines,
IntPtr lpvBits, [In] ref BITMAPINFOHEADER lpbmi, uint fuColorUse); IntPtr lpvBits, [In] ref BITMAPINFOHEADER lpbmi, uint fuColorUse);
[DllImport("kernel32.dll", SetLastError = true)] [DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr hObject); public static extern bool CloseHandle(IntPtr hObject);
@ -1387,27 +1390,27 @@ namespace Avalonia.Win32.Interop
[DllImport("gdi32.dll")] [DllImport("gdi32.dll")]
public static extern int ChoosePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd); public static extern int ChoosePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd);
[DllImport("gdi32.dll")] [DllImport("gdi32.dll")]
public static extern int DescribePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd); public static extern int DescribePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd);
[DllImport("gdi32.dll")] [DllImport("gdi32.dll")]
public static extern int SetPixelFormat(IntPtr hdc, int iPixelFormat, ref PixelFormatDescriptor pfd); public static extern int SetPixelFormat(IntPtr hdc, int iPixelFormat, ref PixelFormatDescriptor pfd);
[DllImport("gdi32.dll")] [DllImport("gdi32.dll")]
public static extern int DescribePixelFormat(IntPtr hdc, int iPixelFormat, int bytes, ref PixelFormatDescriptor pfd); public static extern int DescribePixelFormat(IntPtr hdc, int iPixelFormat, int bytes, ref PixelFormatDescriptor pfd);
[DllImport("gdi32.dll")] [DllImport("gdi32.dll")]
public static extern bool SwapBuffers(IntPtr hdc); public static extern bool SwapBuffers(IntPtr hdc);
[DllImport("opengl32.dll")] [DllImport("opengl32.dll")]
public static extern IntPtr wglCreateContext(IntPtr hdc); public static extern IntPtr wglCreateContext(IntPtr hdc);
[DllImport("opengl32.dll")] [DllImport("opengl32.dll")]
public static extern bool wglDeleteContext(IntPtr context); public static extern bool wglDeleteContext(IntPtr context);
[DllImport("opengl32.dll")] [DllImport("opengl32.dll")]
public static extern bool wglMakeCurrent(IntPtr hdc, IntPtr context); public static extern bool wglMakeCurrent(IntPtr hdc, IntPtr context);
@ -1428,9 +1431,9 @@ namespace Avalonia.Win32.Interop
uint dwMaximumSizeLow, uint dwMaximumSizeLow,
string lpName); string lpName);
[DllImport("msvcrt.dll", EntryPoint="memcpy", SetLastError = false, CallingConvention=CallingConvention.Cdecl)] [DllImport("msvcrt.dll", EntryPoint = "memcpy", SetLastError = false, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count); public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count);
[DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IntPtr target); public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IntPtr target);
@ -1472,10 +1475,10 @@ namespace Avalonia.Win32.Interop
[DllImport("dwmapi.dll")] [DllImport("dwmapi.dll")]
public static extern void DwmFlush(); public static extern void DwmFlush();
[DllImport("dwmapi.dll")] [DllImport("dwmapi.dll")]
public static extern bool DwmDefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult); public static extern bool DwmDefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult);
[DllImport("dwmapi.dll")] [DllImport("dwmapi.dll")]
public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind); public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind);
@ -1542,8 +1545,8 @@ namespace Avalonia.Win32.Interop
throw new Exception("RtlGetVersion failed!"); throw new Exception("RtlGetVersion failed!");
} }
} }
[DllImport("kernel32", EntryPoint="WaitForMultipleObjectsEx", SetLastError = true, CharSet = CharSet.Auto)] [DllImport("kernel32", EntryPoint = "WaitForMultipleObjectsEx", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int IntWaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable); private static extern int IntWaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable);
public const int WAIT_FAILED = unchecked((int)0xFFFFFFFF); public const int WAIT_FAILED = unchecked((int)0xFFFFFFFF);
@ -1551,7 +1554,7 @@ namespace Avalonia.Win32.Interop
internal static int WaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable) internal static int WaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable)
{ {
int result = IntWaitForMultipleObjectsEx(nCount, pHandles, bWaitAll, dwMilliseconds, bAlertable); int result = IntWaitForMultipleObjectsEx(nCount, pHandles, bWaitAll, dwMilliseconds, bAlertable);
if(result == WAIT_FAILED) if (result == WAIT_FAILED)
{ {
throw new Win32Exception(); throw new Win32Exception();
} }
@ -1699,7 +1702,7 @@ namespace Avalonia.Win32.Interop
DrawLeftBorder = 0x20, DrawLeftBorder = 0x20,
DrawTopBorder = 0x40, DrawTopBorder = 0x40,
DrawRightBorder = 0x80, DrawRightBorder = 0x80,
DrawBottomBorder = 0x100, DrawBottomBorder = 0x100,
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
@ -1767,9 +1770,9 @@ namespace Avalonia.Win32.Interop
MDT_ANGULAR_DPI = 1, MDT_ANGULAR_DPI = 1,
MDT_RAW_DPI = 2, MDT_RAW_DPI = 2,
MDT_DEFAULT = MDT_EFFECTIVE_DPI MDT_DEFAULT = MDT_EFFECTIVE_DPI
} }
public enum ClipboardFormat public enum ClipboardFormat
{ {
/// <summary> /// <summary>
/// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. Use this format for ANSI text. /// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. Use this format for ANSI text.
@ -1820,7 +1823,7 @@ namespace Avalonia.Win32.Interop
public int X; public int X;
public int Y; public int Y;
} }
public struct SIZE public struct SIZE
{ {
public int X; public int X;
@ -2021,7 +2024,7 @@ namespace Avalonia.Win32.Interop
OFN_NOREADONLYRETURN = 0x00008000, OFN_NOREADONLYRETURN = 0x00008000,
OFN_OVERWRITEPROMPT = 0x00000002 OFN_OVERWRITEPROMPT = 0x00000002
} }
public enum HRESULT : uint public enum HRESULT : uint
{ {
S_FALSE = 0x0001, S_FALSE = 0x0001,

15
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@ -24,6 +24,7 @@ namespace Avalonia.Win32
private readonly Win32NativeToManagedMenuExporter _exporter; private readonly Win32NativeToManagedMenuExporter _exporter;
private static readonly Dictionary<int, TrayIconImpl> s_trayIcons = new Dictionary<int, TrayIconImpl>(); private static readonly Dictionary<int, TrayIconImpl> s_trayIcons = new Dictionary<int, TrayIconImpl>();
private bool _disposedValue; private bool _disposedValue;
private static readonly uint WM_TASKBARCREATED = UnmanagedMethods.RegisterWindowMessage("TaskbarCreated");
public TrayIconImpl() public TrayIconImpl()
{ {
@ -44,6 +45,18 @@ namespace Avalonia.Win32
{ {
s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam); s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam);
} }
if (msg == WM_TASKBARCREATED)
{
foreach (var tray in s_trayIcons.Values)
{
if (tray._iconAdded)
{
tray.UpdateIcon(true);
tray.UpdateIcon();
}
}
}
} }
public void SetIcon(IWindowIconImpl? icon) public void SetIcon(IWindowIconImpl? icon)
@ -145,7 +158,7 @@ namespace Avalonia.Win32
private enum CustomWindowsMessage : uint private enum CustomWindowsMessage : uint
{ {
WM_TRAYICON = WindowsMessage.WM_APP + 1024, WM_TRAYICON = WindowsMessage.WM_APP + 1024,
WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024,
} }
private class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable private class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable

45
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -6,6 +6,7 @@ using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Threading.Tasks;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
@ -1614,6 +1615,50 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.MoveSelection(NavigationDirection.Next, true); target.MoveSelection(NavigationDirection.Next, true);
} }
[Fact(Timeout = 2000)]
public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_First_Item()
{
var target = new TestSelector
{
Template = Template(),
Items = new[]
{
new ListBoxItem { Focusable = false },
new ListBoxItem(),
}
};
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
// Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below.
// https://github.com/xunit/xunit/issues/2222
await Task.Run(() => target.MoveSelection(NavigationDirection.First, true));
Assert.Equal(-1, target.SelectedIndex);
}
[Fact(Timeout = 2000)]
public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_Last_Item()
{
var target = new TestSelector
{
Template = Template(),
Items = new[]
{
new ListBoxItem(),
new ListBoxItem { Focusable = false },
}
};
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
// Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below.
// https://github.com/xunit/xunit/issues/2222
await Task.Run(() => target.MoveSelection(NavigationDirection.Last, true));
Assert.Equal(-1, target.SelectedIndex);
}
[Fact] [Fact]
public void MoveSelection_Does_Select_Disabled_Controls() public void MoveSelection_Does_Select_Disabled_Controls()
{ {

3
tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs

@ -1,11 +1,8 @@
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform;
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;

119
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -70,12 +70,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
} }
[Fact] [Fact]
public void Should_Get_Next_Caret_CharacterHit_Bidi() public void Should_Get_Next_Caret_CharacterHit_Bidi()
{ {
const string text = "אבג 1 ABC"; const string text = "אבג 1 ABC";
using (Start()) using (Start())
{ {
var defaultProperties = new GenericTextRunProperties(Typeface.Default); var defaultProperties = new GenericTextRunProperties(Typeface.Default);
@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var clusters = new List<int>(); var clusters = new List<int>();
foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start)) foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
{ {
var shapedRun = (ShapedTextCharacters)textRun; var shapedRun = (ShapedTextCharacters)textRun;
@ -98,7 +98,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
shapedRun.ShapedBuffer.GlyphClusters.Reverse() : shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
shapedRun.ShapedBuffer.GlyphClusters); shapedRun.ShapedBuffer.GlyphClusters);
} }
var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]); var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
foreach (var cluster in clusters) foreach (var cluster in clusters)
@ -122,7 +122,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public void Should_Get_Previous_Caret_CharacterHit_Bidi() public void Should_Get_Previous_Caret_CharacterHit_Bidi()
{ {
const string text = "אבג 1 ABC"; const string text = "אבג 1 ABC";
using (Start()) using (Start())
{ {
var defaultProperties = new GenericTextRunProperties(Typeface.Default); var defaultProperties = new GenericTextRunProperties(Typeface.Default);
@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var clusters = new List<int>(); var clusters = new List<int>();
foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start)) foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
{ {
var shapedRun = (ShapedTextCharacters)textRun; var shapedRun = (ShapedTextCharacters)textRun;
@ -147,13 +147,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
clusters.Reverse(); clusters.Reverse();
var nextCharacterHit = new CharacterHit(text.Length - 1); var nextCharacterHit = new CharacterHit(text.Length - 1);
foreach (var cluster in clusters) foreach (var cluster in clusters)
{ {
var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength; var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength;
Assert.Equal(cluster, currentCaretIndex); Assert.Equal(cluster, currentCaretIndex);
nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit); nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit);
@ -168,7 +168,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength); Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength);
} }
} }
[InlineData("𐐷𐐷𐐷𐐷𐐷")] [InlineData("𐐷𐐷𐐷𐐷𐐷")]
[InlineData("01234567🎉\n")] [InlineData("01234567🎉\n")]
[InlineData("𐐷1234")] [InlineData("𐐷1234")]
@ -324,7 +324,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
Assert.Equal(currentDistance,textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length))); Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
} }
} }
@ -371,7 +371,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026"); yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026");
yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026"); yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026");
yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, ""); yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, "");
object[] CreateData(string text, double width, TextTrimming mode, string expected) object[] CreateData(string text, double width, TextTrimming mode, string expected)
{ {
return new object[] return new object[]
@ -424,7 +424,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
var defaultProperties = new GenericTextRunProperties(Typeface.Default); var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new DrawableRunTextSource(); var textSource = new DrawableRunTextSource();
var formatter = new TextFormatterImpl(); var formatter = new TextFormatterImpl();
var textLine = var textLine =
@ -471,7 +471,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(4, textLine.TextRuns.Count); Assert.Equal(4, textLine.TextRuns.Count);
var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3,1)); var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1));
Assert.Equal(3, currentHit.FirstCharacterIndex); Assert.Equal(3, currentHit.FirstCharacterIndex);
Assert.Equal(0, currentHit.TrailingLength); Assert.Equal(0, currentHit.TrailingLength);
@ -552,11 +552,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
switch (textSourceIndex) switch (textSourceIndex)
{ {
case 0: case 0:
return new CustomDrawableRun(); return new CustomDrawableRun();
case 1: case 1:
return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default)); return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default));
case 2: case 2:
return new CustomDrawableRun(); return new CustomDrawableRun();
case 3: case 3:
return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default)); return new TextCharacters(new ReadOnlySlice<char>(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default));
default: default:
@ -564,14 +564,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
} }
private class CustomDrawableRun : DrawableTextRun private class CustomDrawableRun : DrawableTextRun
{ {
public override Size Size => new(14, 14); public override Size Size => new(14, 14);
public override double Baseline => 14; public override double Baseline => 14;
public override void Draw(DrawingContext drawingContext, Point origin) public override void Draw(DrawingContext drawingContext, Point origin)
{ {
} }
} }
@ -587,29 +587,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var shapedTextRuns = textLine.TextRuns.Cast<ShapedTextCharacters>().ToList(); var shapedTextRuns = textLine.TextRuns.Cast<ShapedTextCharacters>().ToList();
var lastCluster = -1; var lastCluster = -1;
foreach (var textRun in shapedTextRuns) foreach (var textRun in shapedTextRuns)
{ {
var shapedBuffer = textRun.ShapedBuffer; var shapedBuffer = textRun.ShapedBuffer;
var currentClusters = shapedBuffer.GlyphClusters.ToList(); var currentClusters = shapedBuffer.GlyphClusters.ToList();
foreach (var currentCluster in currentClusters) foreach (var currentCluster in currentClusters)
{ {
if (lastCluster == currentCluster) if (lastCluster == currentCluster)
{ {
continue; continue;
} }
glyphClusters.Add(currentCluster); glyphClusters.Add(currentCluster);
lastCluster = currentCluster; lastCluster = currentCluster;
} }
} }
return glyphClusters; return glyphClusters;
} }
private static List<Rect> BuildRects(TextLine textLine) private static List<Rect> BuildRects(TextLine textLine)
{ {
var rects = new List<Rect>(); var rects = new List<Rect>();
@ -624,11 +624,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
foreach (var textRun in shapedTextRuns) foreach (var textRun in shapedTextRuns)
{ {
var shapedBuffer = textRun.ShapedBuffer; var shapedBuffer = textRun.ShapedBuffer;
for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++) for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++)
{ {
var currentCluster = shapedBuffer.GlyphClusters[index]; var currentCluster = shapedBuffer.GlyphClusters[index];
var advance = shapedBuffer.GlyphAdvances[index]; var advance = shapedBuffer.GlyphAdvances[index];
if (lastCluster != currentCluster) if (lastCluster != currentCluster)
@ -642,10 +642,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
rects.Remove(rect); rects.Remove(rect);
rect = rect.WithWidth(rect.Width + advance); rect = rect.WithWidth(rect.Width + advance);
rects.Add(rect); rects.Add(rect);
} }
currentX += advance; currentX += advance;
lastCluster = currentCluster; lastCluster = currentCluster;
@ -655,8 +655,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
return rects; return rects;
} }
[Fact]
public void Should_Get_TextBounds_Mixed()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var text = "0123".AsMemory();
var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture);
var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, 1, text.Length), shaperOption), defaultProperties);
var textRuns = new List<TextRun>
{
new CustomDrawableRun(),
firstRun,
new CustomDrawableRun(),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length + 2, text.Length), shaperOption), defaultProperties),
new CustomDrawableRun(),
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 2 + 3, text.Length), shaperOption), defaultProperties)
};
var textSource = new FixedRunsTextSource(textRuns);
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3);
Assert.Equal(1, textBounds.Count);
Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
textBounds = textLine.GetTextBounds(0, 1);
Assert.Equal(1, textBounds.Count);
Assert.Equal(14, textBounds[0].Rectangle.Width);
textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1);
Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width);
textBounds = textLine.GetTextBounds(1, firstRun.Text.Length);
Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1);
Assert.Equal(1, textBounds.Count);
Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width);
}
}
[Fact] [Fact]
public void Should_Get_TextBounds() public void Should_Get_TextBounds_BiDi()
{ {
using (Start()) using (Start())
{ {
@ -673,7 +730,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 3, text.Length), ltrOptions), defaultProperties) new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice<char>(text, text.Length * 3, text.Length), ltrOptions), defaultProperties)
}; };
var textSource = new FixedRunsTextSource(textRuns); var textSource = new FixedRunsTextSource(textRuns);
var formatter = new TextFormatterImpl(); var formatter = new TextFormatterImpl();
@ -700,12 +757,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public TextRun? GetTextRun(int textSourceIndex) public TextRun? GetTextRun(int textSourceIndex)
{ {
var currentPosition = 0;
foreach (var textRun in _textRuns) foreach (var textRun in _textRuns)
{ {
if(textRun.Text.Start == textSourceIndex) if (currentPosition == textSourceIndex)
{ {
return textRun; return textRun;
} }
currentPosition += textRun.TextSourceLength;
} }
return null; return null;

BIN
tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 752 B

BIN
tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 B

After

Width:  |  Height:  |  Size: 557 B

Loading…
Cancel
Save