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