committed by
GitHub
214 changed files with 9286 additions and 2951 deletions
@ -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
|
|||
@ -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
|
|||
@ -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
|
|||
@ -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]; |
|||
} |
|||
@ -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
|
|||
@ -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()]; |
|||
} |
|||
@ -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
|
|||
@ -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; |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,79 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls" |
|||
xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker" |
|||
mc:Ignorable="d" |
|||
d:DesignWidth="800" |
|||
d:DesignHeight="450" |
|||
x:Class="ControlCatalog.Pages.ColorPickerPage"> |
|||
|
|||
<UserControl.Resources> |
|||
<pc:ThirdComponentConverter x:Key="ThirdComponent" /> |
|||
</UserControl.Resources> |
|||
|
|||
<Grid ColumnDefinitions="Auto,10,Auto"> |
|||
<Grid Grid.Column="0" |
|||
Grid.Row="0" |
|||
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto"> |
|||
<ColorSpectrum x:Name="ColorSpectrum1" |
|||
Grid.Row="0" |
|||
Color="Red" |
|||
CornerRadius="10" |
|||
Height="256" |
|||
Width="256" /> |
|||
<ColorSlider Grid.Row="1" |
|||
Margin="0,10,0,0" |
|||
ColorComponent="Component1" |
|||
ColorModel="Hsva" |
|||
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" /> |
|||
<ColorSlider Grid.Row="2" |
|||
ColorComponent="Component2" |
|||
ColorModel="Hsva" |
|||
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" /> |
|||
<ColorSlider Grid.Row="3" |
|||
ColorComponent="Component3" |
|||
ColorModel="Hsva" |
|||
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" /> |
|||
<ColorSlider Grid.Row="4" |
|||
ColorComponent="Alpha" |
|||
ColorModel="Hsva" |
|||
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" /> |
|||
<ColorPreviewer Grid.Row="5" |
|||
ShowAccentColors="True" |
|||
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" /> |
|||
</Grid> |
|||
<Grid Grid.Column="2" |
|||
Grid.Row="0" |
|||
ColumnDefinitions="Auto,Auto,Auto" |
|||
RowDefinitions="Auto,Auto"> |
|||
<ColorSlider Grid.Column="0" |
|||
Grid.Row="0" |
|||
IsAlphaMaxForced="True" |
|||
IsSaturationValueMaxForced="False" |
|||
ColorComponent="{Binding Components, ElementName=ColorSpectrum2, Converter={StaticResource ThirdComponent}}" |
|||
ColorModel="Hsva" |
|||
Orientation="Vertical" |
|||
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" /> |
|||
<ColorSpectrum x:Name="ColorSpectrum2" |
|||
Grid.Column="1" |
|||
Grid.Row="0" |
|||
Color="Green" |
|||
Shape="Ring" |
|||
Height="256" |
|||
Width="256" /> |
|||
<ColorSlider Grid.Column="2" |
|||
Grid.Row="0" |
|||
ColorComponent="Alpha" |
|||
ColorModel="Hsva" |
|||
Orientation="Vertical" |
|||
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" /> |
|||
<ColorPreviewer Grid.Column="0" |
|||
Grid.ColumnSpan="3" |
|||
Grid.Row="1" |
|||
ShowAccentColors="True" |
|||
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" /> |
|||
</Grid> |
|||
</Grid> |
|||
</UserControl> |
|||
@ -0,0 +1,19 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public partial class ColorPickerPage : UserControl |
|||
{ |
|||
public ColorPickerPage() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// Provides extensions for <see cref="AvaloniaPropertyChangedEventArgs"/>.
|
|||
/// </summary>
|
|||
public static class AvaloniaPropertyChangedExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets a typed value from <see cref="AvaloniaPropertyChangedEventArgs.OldValue"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The value type.</typeparam>
|
|||
/// <param name="e">The event args.</param>
|
|||
/// <returns>The value.</returns>
|
|||
public static T GetOldValue<T>(this AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
return ((AvaloniaPropertyChangedEventArgs<T>)e).OldValue.GetValueOrDefault()!; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a typed value from <see cref="AvaloniaPropertyChangedEventArgs.NewValue"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The value type.</typeparam>
|
|||
/// <param name="e">The event args.</param>
|
|||
/// <returns>The value.</returns>
|
|||
public static T GetNewValue<T>(this AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
return ((AvaloniaPropertyChangedEventArgs<T>)e).NewValue.GetValueOrDefault()!; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a typed value from <see cref="AvaloniaPropertyChangedEventArgs.OldValue"/> and
|
|||
/// <see cref="AvaloniaPropertyChangedEventArgs.NewValue"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The value type.</typeparam>
|
|||
/// <param name="e">The event args.</param>
|
|||
/// <returns>The value.</returns>
|
|||
public static (T oldValue, T newValue) GetOldAndNewValue<T>(this AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
var ev = (AvaloniaPropertyChangedEventArgs<T>)e; |
|||
return (ev.OldValue.GetValueOrDefault()!, ev.NewValue.GetValueOrDefault()!); |
|||
} |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an entity that can receive change notifications in a <see cref="ValueStore"/>.
|
|||
/// </summary>
|
|||
internal interface IValueSink |
|||
{ |
|||
void ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change); |
|||
|
|||
void Completed<T>( |
|||
StyledPropertyBase<T> property, |
|||
IPriorityValueEntry entry, |
|||
Optional<T> oldValue); |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a union type of <see cref="ValueStore"/> and <see cref="PriorityValue{T}"/>,
|
|||
/// which are the valid owners of a value store <see cref="IValue"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The value type.</typeparam>
|
|||
internal readonly struct ValueOwner<T> |
|||
{ |
|||
private readonly ValueStore? _store; |
|||
private readonly PriorityValue<T>? _priorityValue; |
|||
|
|||
public ValueOwner(ValueStore o) |
|||
{ |
|||
_store = o; |
|||
_priorityValue = null; |
|||
} |
|||
|
|||
public ValueOwner(PriorityValue<T> v) |
|||
{ |
|||
_store = null; |
|||
_priorityValue = v; |
|||
} |
|||
|
|||
public bool IsValueStore => _store is not null; |
|||
|
|||
public void Completed(StyledPropertyBase<T> property, IPriorityValueEntry entry, Optional<T> oldValue) |
|||
{ |
|||
if (_store is not null) |
|||
_store?.Completed(property, entry, oldValue); |
|||
else |
|||
_priorityValue!.Completed(entry, oldValue); |
|||
} |
|||
|
|||
public void ValueChanged(AvaloniaPropertyChangedEventArgs<T> e) |
|||
{ |
|||
if (_store is not null) |
|||
_store?.ValueChanged(e); |
|||
else |
|||
_priorityValue!.ValueChanged(e); |
|||
} |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// A visitor to resolve an untyped <see cref="AvaloniaProperty"/> to a typed property.
|
|||
/// </summary>
|
|||
/// <typeparam name="TData">The type of user data passed.</typeparam>
|
|||
/// <remarks>
|
|||
/// Pass an instance that implements this interface to
|
|||
/// <see cref="AvaloniaProperty.Accept{TData}(IAvaloniaPropertyVisitor{TData}, ref TData)"/>
|
|||
/// in order to resolve un untyped <see cref="AvaloniaProperty"/> to a typed
|
|||
/// <see cref="StyledPropertyBase{TValue}"/> or <see cref="DirectPropertyBase{TValue}"/>.
|
|||
/// </remarks>
|
|||
public interface IAvaloniaPropertyVisitor<TData> |
|||
where TData : struct |
|||
{ |
|||
/// <summary>
|
|||
/// Called when the property is a styled property.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The property value type.</typeparam>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <param name="data">The user data.</param>
|
|||
void Visit<T>(StyledPropertyBase<T> property, ref TData data); |
|||
|
|||
/// <summary>
|
|||
/// Called when the property is a direct property.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The property value type.</typeparam>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <param name="data">The user data.</param>
|
|||
void Visit<T>(DirectPropertyBase<T> property, ref TData data); |
|||
} |
|||
} |
|||
@ -0,0 +1,241 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Collections.Pooled; |
|||
|
|||
namespace Avalonia.Utilities; |
|||
|
|||
internal class WeakHashList<T> where T : class |
|||
{ |
|||
public const int DefaultArraySize = 8; |
|||
|
|||
private struct Key |
|||
{ |
|||
public WeakReference<T>? Weak; |
|||
public T? Strong; |
|||
public int HashCode; |
|||
|
|||
public static Key MakeStrong(T r) => new() |
|||
{ |
|||
HashCode = r.GetHashCode(), |
|||
Strong = r |
|||
}; |
|||
|
|||
public static Key MakeWeak(T r) => new() |
|||
{ |
|||
HashCode = r.GetHashCode(), |
|||
Weak = new WeakReference<T>(r) |
|||
}; |
|||
|
|||
public override int GetHashCode() => HashCode; |
|||
} |
|||
|
|||
class KeyComparer : IEqualityComparer<Key> |
|||
{ |
|||
public bool Equals(Key x, Key y) |
|||
{ |
|||
if (x.HashCode != y.HashCode) |
|||
return false; |
|||
if (x.Strong != null) |
|||
{ |
|||
if (y.Strong != null) |
|||
return x.Strong == y.Strong; |
|||
if (y.Weak == null) |
|||
return false; |
|||
return y.Weak.TryGetTarget(out var weakTarget) && weakTarget == x.Strong; |
|||
} |
|||
else if (y.Strong != null) |
|||
{ |
|||
if (x.Weak == null) |
|||
return false; |
|||
return x.Weak.TryGetTarget(out var weakTarget) && weakTarget == y.Strong; |
|||
} |
|||
else |
|||
{ |
|||
if (x.Weak == null || x.Weak.TryGetTarget(out var xTarget) == false) |
|||
return y.Weak?.TryGetTarget(out _) != true; |
|||
return y.Weak?.TryGetTarget(out var yTarget) == true && xTarget == yTarget; |
|||
} |
|||
} |
|||
|
|||
public int GetHashCode(Key obj) => obj.HashCode; |
|||
public static KeyComparer Instance = new(); |
|||
} |
|||
|
|||
Dictionary<Key, int>? _dic; |
|||
WeakReference<T>?[]? _arr; |
|||
int _arrCount; |
|||
|
|||
public bool IsEmpty => _dic is not null ? _dic.Count == 0 : _arrCount == 0; |
|||
|
|||
public bool NeedCompact { get; private set; } |
|||
|
|||
public void Add(T item) |
|||
{ |
|||
if (_dic != null) |
|||
{ |
|||
var strongKey = Key.MakeStrong(item); |
|||
if (_dic.TryGetValue(strongKey, out var cnt)) |
|||
_dic[strongKey] = cnt + 1; |
|||
else |
|||
_dic[Key.MakeWeak(item)] = 1; |
|||
return; |
|||
} |
|||
|
|||
if (_arr == null) |
|||
_arr = new WeakReference<T>[DefaultArraySize]; |
|||
|
|||
if (_arrCount < _arr.Length) |
|||
{ |
|||
_arr[_arrCount] = new WeakReference<T>(item); |
|||
_arrCount++; |
|||
return; |
|||
} |
|||
|
|||
// Check if something is dead
|
|||
for (var c = 0; c < _arrCount; c++) |
|||
{ |
|||
if (_arr[c]!.TryGetTarget(out _) == false) |
|||
{ |
|||
_arr[c] = new WeakReference<T>(item); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
_dic = new Dictionary<Key, int>(KeyComparer.Instance); |
|||
foreach (var existing in _arr) |
|||
{ |
|||
if (existing!.TryGetTarget(out var target)) |
|||
Add(target); |
|||
} |
|||
|
|||
Add(item); |
|||
|
|||
_arr = null; |
|||
_arrCount = 0; |
|||
} |
|||
|
|||
public void Remove(T item) |
|||
{ |
|||
if (_arr != null) |
|||
{ |
|||
for (var c = 0; c < _arrCount; c++) |
|||
{ |
|||
if (_arr[c]?.TryGetTarget(out var target) == true && target == item) |
|||
{ |
|||
_arr[c] = null; |
|||
ArrCompact(); |
|||
return; |
|||
} |
|||
} |
|||
} |
|||
else if (_dic != null) |
|||
{ |
|||
var strongKey = Key.MakeStrong(item); |
|||
|
|||
if (_dic.TryGetValue(strongKey, out var cnt)) |
|||
{ |
|||
if (cnt > 1) |
|||
{ |
|||
_dic[strongKey] = cnt - 1; |
|||
return; |
|||
} |
|||
} |
|||
|
|||
_dic.Remove(strongKey); |
|||
} |
|||
} |
|||
|
|||
private void ArrCompact() |
|||
{ |
|||
if (_arr != null) |
|||
{ |
|||
int empty = -1; |
|||
for (var c = 0; c < _arrCount; c++) |
|||
{ |
|||
var r = _arr[c]; |
|||
//Mark current index as first empty
|
|||
if (r == null && empty == -1) |
|||
empty = c; |
|||
//If current element isn't null and we have an empty one
|
|||
if (r != null && empty != -1) |
|||
{ |
|||
_arr[c] = null; |
|||
_arr[empty] = r; |
|||
empty++; |
|||
} |
|||
} |
|||
|
|||
if (empty != -1) |
|||
_arrCount = empty; |
|||
} |
|||
} |
|||
|
|||
public void Compact() |
|||
{ |
|||
if (_dic != null) |
|||
{ |
|||
PooledList<Key>? toRemove = null; |
|||
foreach (var kvp in _dic) |
|||
{ |
|||
if (kvp.Key.Weak?.TryGetTarget(out _) != true) |
|||
(toRemove ??= new PooledList<Key>()).Add(kvp.Key); |
|||
} |
|||
|
|||
if (toRemove != null) |
|||
{ |
|||
foreach (var k in toRemove) |
|||
_dic.Remove(k); |
|||
toRemove.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static readonly Stack<PooledList<T>> s_listPool = new(); |
|||
|
|||
public static void ReturnToSharedPool(PooledList<T> list) |
|||
{ |
|||
list.Clear(); |
|||
s_listPool.Push(list); |
|||
} |
|||
|
|||
public PooledList<T>? GetAlive(Func<PooledList<T>>? factory = null) |
|||
{ |
|||
PooledList<T>? pooled = null; |
|||
if (_arr != null) |
|||
{ |
|||
bool needCompact = false; |
|||
for (var c = 0; c < _arrCount; c++) |
|||
{ |
|||
if (_arr[c]?.TryGetTarget(out var target) == true) |
|||
(pooled ??= factory?.Invoke() |
|||
?? (s_listPool.Count > 0 |
|||
? s_listPool.Pop() |
|||
: new PooledList<T>())).Add(target!); |
|||
else |
|||
{ |
|||
_arr[c] = null; |
|||
needCompact = true; |
|||
} |
|||
} |
|||
if(needCompact) |
|||
ArrCompact(); |
|||
return pooled; |
|||
} |
|||
if (_dic != null) |
|||
{ |
|||
foreach (var kvp in _dic) |
|||
{ |
|||
if (kvp.Key.Weak?.TryGetTarget(out var target) == true) |
|||
(pooled ??= factory?.Invoke() |
|||
?? (s_listPool.Count > 0 |
|||
? s_listPool.Pop() |
|||
: new PooledList<T>())) |
|||
.Add(target!); |
|||
else |
|||
NeedCompact = true; |
|||
} |
|||
} |
|||
|
|||
return pooled; |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks> |
|||
<PackageId>Avalonia.Controls.ColorPicker</PackageId> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" /> |
|||
<ProjectReference Include="..\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj" /> |
|||
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" /> |
|||
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" /> |
|||
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" /> |
|||
<!-- Compatibility with old apps --> |
|||
<EmbeddedResource Include="Themes\**\*.xaml" /> |
|||
</ItemGroup> |
|||
<Import Project="..\..\build\Rx.props" /> |
|||
<Import Project="..\..\build\EmbedXaml.props" /> |
|||
<Import Project="..\..\build\JetBrains.Annotations.props" /> |
|||
<Import Project="..\..\build\BuildTargets.targets" /> |
|||
<!--<Import Project="..\..\build\ApiDiff.props" />--> |
|||
<Import Project="..\..\build\NullableEnable.props" /> |
|||
<Import Project="..\..\build\DevAnalyzers.props" /> |
|||
</Project> |
|||
@ -0,0 +1,41 @@ |
|||
// Portions of this source file are adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under the MIT License.
|
|||
|
|||
using System; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Holds the details of a ColorChanged event.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// HSV color information is intentionally not provided.
|
|||
/// Use <see cref="Color.ToHsv()"/> to obtain it.
|
|||
/// </remarks>
|
|||
public class ColorChangedEventArgs : EventArgs |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ColorChangedEventArgs"/> class.
|
|||
/// </summary>
|
|||
/// <param name="oldColor">The old/original color from before the change event.</param>
|
|||
/// <param name="newColor">The new/updated color that triggered the change event.</param>
|
|||
public ColorChangedEventArgs(Color oldColor, Color newColor) |
|||
{ |
|||
OldColor = oldColor; |
|||
NewColor = newColor; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the old/original color from before the change event.
|
|||
/// </summary>
|
|||
public Color OldColor { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the new/updated color that triggered the change event.
|
|||
/// </summary>
|
|||
public Color NewColor { get; private set; } |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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())); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,222 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under the MIT License.
|
|||
|
|||
using Avalonia.Data; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public partial class ColorSpectrum |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="Color"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<Color> ColorProperty = |
|||
AvaloniaProperty.Register<ColorSpectrum, Color>( |
|||
nameof(Color), |
|||
Colors.White, |
|||
defaultBindingMode: BindingMode.TwoWay); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Components"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<ColorSpectrumComponents> ComponentsProperty = |
|||
AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumComponents>( |
|||
nameof(Components), |
|||
ColorSpectrumComponents.HueSaturation); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="HsvColor"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<HsvColor> HsvColorProperty = |
|||
AvaloniaProperty.Register<ColorSpectrum, HsvColor>( |
|||
nameof(HsvColor), |
|||
Colors.White.ToHsv(), |
|||
defaultBindingMode: BindingMode.TwoWay); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="MaxHue"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<int> MaxHueProperty = |
|||
AvaloniaProperty.Register<ColorSpectrum, int>( |
|||
nameof(MaxHue), |
|||
359); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="MaxSaturation"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<int> MaxSaturationProperty = |
|||
AvaloniaProperty.Register<ColorSpectrum, int>( |
|||
nameof(MaxSaturation), |
|||
100); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="MaxValue"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<int> MaxValueProperty = |
|||
AvaloniaProperty.Register<ColorSpectrum, int>( |
|||
nameof(MaxValue), |
|||
100); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="MinHue"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<int> MinHueProperty = |
|||
AvaloniaProperty.Register<ColorSpectrum, int>( |
|||
nameof(MinHue), |
|||
0); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="MinSaturation"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<int> MinSaturationProperty = |
|||
AvaloniaProperty.Register<ColorSpectrum, int>( |
|||
nameof(MinSaturation), |
|||
0); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="MinValue"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<int> MinValueProperty = |
|||
AvaloniaProperty.Register<ColorSpectrum, int>( |
|||
nameof(MinValue), |
|||
0); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Shape"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<ColorSpectrumShape> ShapeProperty = |
|||
AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumShape>( |
|||
nameof(Shape), |
|||
ColorSpectrumShape.Box); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the currently selected color in the RGB color model.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// For control authors use <see cref="HsvColor"/> instead to avoid loss
|
|||
/// of precision and color drifting.
|
|||
/// </remarks>
|
|||
public Color Color |
|||
{ |
|||
get => GetValue(ColorProperty); |
|||
set => SetValue(ColorProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the two HSV color components displayed by the spectrum.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
|
|||
/// </remarks>
|
|||
public ColorSpectrumComponents Components |
|||
{ |
|||
get => GetValue(ComponentsProperty); |
|||
set => SetValue(ComponentsProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the currently selected color in the HSV color model.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This should be used in all cases instead of the <see cref="Color"/> property.
|
|||
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model and using
|
|||
/// this property will avoid loss of precision and color drifting.
|
|||
/// </remarks>
|
|||
public HsvColor HsvColor |
|||
{ |
|||
get => GetValue(HsvColorProperty); |
|||
set => SetValue(HsvColorProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the maximum value of the Hue component in the range from 0..359.
|
|||
/// This property must be greater than <see cref="MinHue"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
|
|||
/// </remarks>
|
|||
public int MaxHue |
|||
{ |
|||
get => GetValue(MaxHueProperty); |
|||
set => SetValue(MaxHueProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the maximum value of the Saturation component in the range from 0..100.
|
|||
/// This property must be greater than <see cref="MinSaturation"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
|
|||
/// </remarks>
|
|||
public int MaxSaturation |
|||
{ |
|||
get => GetValue(MaxSaturationProperty); |
|||
set => SetValue(MaxSaturationProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the maximum value of the Value component in the range from 0..100.
|
|||
/// This property must be greater than <see cref="MinValue"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
|
|||
/// </remarks>
|
|||
public int MaxValue |
|||
{ |
|||
get => GetValue(MaxValueProperty); |
|||
set => SetValue(MaxValueProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the minimum value of the Hue component in the range from 0..359.
|
|||
/// This property must be less than <see cref="MaxHue"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
|
|||
/// </remarks>
|
|||
public int MinHue |
|||
{ |
|||
get => GetValue(MinHueProperty); |
|||
set => SetValue(MinHueProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the minimum value of the Saturation component in the range from 0..100.
|
|||
/// This property must be less than <see cref="MaxSaturation"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
|
|||
/// </remarks>
|
|||
public int MinSaturation |
|||
{ |
|||
get => GetValue(MinSaturationProperty); |
|||
set => SetValue(MinSaturationProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the minimum value of the Value component in the range from 0..100.
|
|||
/// This property must be less than <see cref="MaxValue"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
|
|||
/// </remarks>
|
|||
public int MinValue |
|||
{ |
|||
get => GetValue(MinValueProperty); |
|||
set => SetValue(MinValueProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the displayed shape of the spectrum.
|
|||
/// </summary>
|
|||
public ColorSpectrumShape Shape |
|||
{ |
|||
get => GetValue(ShapeProperty); |
|||
set => SetValue(ShapeProperty, value); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,73 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under the MIT License.
|
|||
|
|||
using Avalonia.Controls.Primitives; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the two HSV color components displayed by a <see cref="ColorSpectrum"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Order of the color components is important and correspond with an X/Y axis in Box
|
|||
/// shape or a degree/radius in Ring shape.
|
|||
/// </remarks>
|
|||
public enum ColorSpectrumComponents |
|||
{ |
|||
/// <summary>
|
|||
/// The Hue and Value components.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// In Box shape, Hue is mapped to the X-axis and Value is mapped to the Y-axis.
|
|||
/// In Ring shape, Hue is mapped to degrees and Value is mapped to radius.
|
|||
/// </remarks>
|
|||
HueValue, |
|||
|
|||
/// <summary>
|
|||
/// The Value and Hue components.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// In Box shape, Value is mapped to the X-axis and Hue is mapped to the Y-axis.
|
|||
/// In Ring shape, Value is mapped to degrees and Hue is mapped to radius.
|
|||
/// </remarks>
|
|||
ValueHue, |
|||
|
|||
/// <summary>
|
|||
/// The Hue and Saturation components.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// In Box shape, Hue is mapped to the X-axis and Saturation is mapped to the Y-axis.
|
|||
/// In Ring shape, Hue is mapped to degrees and Saturation is mapped to radius.
|
|||
/// </remarks>
|
|||
HueSaturation, |
|||
|
|||
/// <summary>
|
|||
/// The Saturation and Hue components.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// In Box shape, Saturation is mapped to the X-axis and Hue is mapped to the Y-axis.
|
|||
/// In Ring shape, Saturation is mapped to degrees and Hue is mapped to radius.
|
|||
/// </remarks>
|
|||
SaturationHue, |
|||
|
|||
/// <summary>
|
|||
/// The Saturation and Value components.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// In Box shape, Saturation is mapped to the X-axis and Value is mapped to the Y-axis.
|
|||
/// In Ring shape, Saturation is mapped to degrees and Value is mapped to radius.
|
|||
/// </remarks>
|
|||
SaturationValue, |
|||
|
|||
/// <summary>
|
|||
/// The Value and Saturation components.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// In Box shape, Value is mapped to the X-axis and Saturation is mapped to the Y-axis.
|
|||
/// In Ring shape, Value is mapped to degrees and Saturation is mapped to radius.
|
|||
/// </remarks>
|
|||
ValueSaturation, |
|||
}; |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under the MIT License.
|
|||
|
|||
using Avalonia.Controls.Primitives; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the shape of a <see cref="ColorSpectrum"/>.
|
|||
/// </summary>
|
|||
public enum ColorSpectrumShape |
|||
{ |
|||
/// <summary>
|
|||
/// The spectrum is in the shape of a rectangular or square box.
|
|||
/// Note that more colors are visible to the user in Box shape.
|
|||
/// </summary>
|
|||
Box, |
|||
|
|||
/// <summary>
|
|||
/// The spectrum is in the shape of an ellipse or circle.
|
|||
/// </summary>
|
|||
Ring, |
|||
}; |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Data.Converters; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Controls.Converters |
|||
{ |
|||
/// <summary>
|
|||
/// Converts the given value into an <see cref="IBrush"/> when a conversion is possible.
|
|||
/// </summary>
|
|||
public class ToBrushConverter : IValueConverter |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public object? Convert( |
|||
object? value, |
|||
Type targetType, |
|||
object? parameter, |
|||
CultureInfo culture) |
|||
{ |
|||
if (value is IBrush brush) |
|||
{ |
|||
return brush; |
|||
} |
|||
else if (value is Color valueColor) |
|||
{ |
|||
return new SolidColorBrush(valueColor); |
|||
} |
|||
else if (value is HslColor valueHslColor) |
|||
{ |
|||
return new SolidColorBrush(valueHslColor.ToRgb()); |
|||
} |
|||
else if (value is HsvColor valueHsvColor) |
|||
{ |
|||
return new SolidColorBrush(valueHsvColor.ToRgb()); |
|||
} |
|||
|
|||
return AvaloniaProperty.UnsetValue; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public object? ConvertBack( |
|||
object? value, |
|||
Type targetType, |
|||
object? parameter, |
|||
CultureInfo culture) |
|||
{ |
|||
return AvaloniaProperty.UnsetValue; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue