175 changed files with 7706 additions and 4191 deletions
@ -1,5 +1,5 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="7.9.0.2" /> |
|||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.1" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,17 @@ |
|||
//
|
|||
// Created by Dan Walmsley on 05/05/2022.
|
|||
// Copyright (c) 2022 Avalonia. All rights reserved.
|
|||
//
|
|||
|
|||
#pragma once |
|||
|
|||
#import <Foundation/Foundation.h> |
|||
#include "avalonia-native.h" |
|||
|
|||
@interface AutoFitContentView : NSView |
|||
-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; |
|||
-(void) ShowTitleBar: (bool) show; |
|||
-(void) SetTitleBarHeightHint: (double) height; |
|||
|
|||
-(void) ShowBlur: (bool) show; |
|||
@end |
|||
@ -0,0 +1,106 @@ |
|||
// |
|||
// Created by Dan Walmsley on 05/05/2022. |
|||
// Copyright (c) 2022 Avalonia. All rights reserved. |
|||
// |
|||
|
|||
#include "AvnView.h" |
|||
#include "AutoFitContentView.h" |
|||
#include "WindowInterfaces.h" |
|||
#include "WindowProtocol.h" |
|||
|
|||
@implementation AutoFitContentView |
|||
{ |
|||
NSVisualEffectView* _titleBarMaterial; |
|||
NSBox* _titleBarUnderline; |
|||
NSView* _content; |
|||
NSVisualEffectView* _blurBehind; |
|||
double _titleBarHeightHint; |
|||
bool _settingSize; |
|||
} |
|||
|
|||
-(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content |
|||
{ |
|||
_titleBarHeightHint = -1; |
|||
_content = content; |
|||
_settingSize = false; |
|||
|
|||
[self setAutoresizesSubviews:true]; |
|||
[self setWantsLayer:true]; |
|||
|
|||
_titleBarMaterial = [NSVisualEffectView new]; |
|||
[_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow]; |
|||
[_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar]; |
|||
[_titleBarMaterial setWantsLayer:true]; |
|||
_titleBarMaterial.hidden = true; |
|||
|
|||
_titleBarUnderline = [NSBox new]; |
|||
_titleBarUnderline.boxType = NSBoxSeparator; |
|||
_titleBarUnderline.fillColor = [NSColor underPageBackgroundColor]; |
|||
_titleBarUnderline.hidden = true; |
|||
|
|||
[self addSubview:_titleBarMaterial]; |
|||
[self addSubview:_titleBarUnderline]; |
|||
|
|||
_blurBehind = [NSVisualEffectView new]; |
|||
[_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; |
|||
[_blurBehind setMaterial:NSVisualEffectMaterialLight]; |
|||
[_blurBehind setWantsLayer:true]; |
|||
_blurBehind.hidden = true; |
|||
|
|||
[_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; |
|||
[_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; |
|||
|
|||
[self addSubview:_blurBehind]; |
|||
[self addSubview:_content]; |
|||
|
|||
[self setWantsLayer:true]; |
|||
return self; |
|||
} |
|||
|
|||
-(void) ShowBlur:(bool)show |
|||
{ |
|||
_blurBehind.hidden = !show; |
|||
} |
|||
|
|||
-(void) ShowTitleBar: (bool) show |
|||
{ |
|||
_titleBarMaterial.hidden = !show; |
|||
_titleBarUnderline.hidden = !show; |
|||
} |
|||
|
|||
-(void) SetTitleBarHeightHint: (double) height |
|||
{ |
|||
_titleBarHeightHint = height; |
|||
|
|||
[self setFrameSize:self.frame.size]; |
|||
} |
|||
|
|||
-(void)setFrameSize:(NSSize)newSize |
|||
{ |
|||
if(_settingSize) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_settingSize = true; |
|||
[super setFrameSize:newSize]; |
|||
|
|||
auto window = static_cast<id <AvnWindowProtocol>>([self window]); |
|||
|
|||
// TODO get actual titlebar size |
|||
|
|||
double height = _titleBarHeightHint == -1 ? [window getExtendedTitleBarHeight] : _titleBarHeightHint; |
|||
|
|||
NSRect tbar; |
|||
tbar.origin.x = 0; |
|||
tbar.origin.y = newSize.height - height; |
|||
tbar.size.width = newSize.width; |
|||
tbar.size.height = height; |
|||
|
|||
[_titleBarMaterial setFrame:tbar]; |
|||
tbar.size.height = height < 1 ? 0 : 1; |
|||
[_titleBarUnderline setFrame:tbar]; |
|||
|
|||
_settingSize = false; |
|||
} |
|||
@end |
|||
@ -0,0 +1,11 @@ |
|||
// |
|||
// Created by Dan Walmsley on 06/05/2022. |
|||
// Copyright (c) 2022 Avalonia. All rights reserved. |
|||
// |
|||
|
|||
#pragma once |
|||
|
|||
#define IS_NSPANEL |
|||
|
|||
#include "AvnWindow.mm" |
|||
|
|||
@ -0,0 +1,27 @@ |
|||
//
|
|||
// Created by Dan Walmsley on 05/05/2022.
|
|||
// Copyright (c) 2022 Avalonia. All rights reserved.
|
|||
//
|
|||
#pragma once |
|||
#import <Foundation/Foundation.h> |
|||
|
|||
|
|||
#import <Foundation/Foundation.h> |
|||
#import <AppKit/AppKit.h> |
|||
#include "common.h" |
|||
#include "WindowImpl.h" |
|||
#include "KeyTransform.h" |
|||
|
|||
@class AvnAccessibilityElement; |
|||
|
|||
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination> |
|||
-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; |
|||
-(NSEvent* _Nonnull) lastMouseDownEvent; |
|||
-(AvnPoint) translateLocalPoint:(AvnPoint)pt; |
|||
-(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; |
|||
-(void) onClosed; |
|||
|
|||
-(AvnPlatformResizeReason) getResizeReason; |
|||
-(void) setResizeReason:(AvnPlatformResizeReason)reason; |
|||
+ (AvnPoint)toAvnPoint:(CGPoint)p; |
|||
@end |
|||
@ -0,0 +1,712 @@ |
|||
// |
|||
// Created by Dan Walmsley on 05/05/2022. |
|||
// Copyright (c) 2022 Avalonia. All rights reserved. |
|||
// |
|||
|
|||
#import <AppKit/AppKit.h> |
|||
#include "AvnView.h" |
|||
#include "automation.h" |
|||
#import "WindowInterfaces.h" |
|||
|
|||
@implementation AvnView |
|||
{ |
|||
ComPtr<WindowBaseImpl> _parent; |
|||
NSTrackingArea* _area; |
|||
bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; |
|||
AvnInputModifiers _modifierState; |
|||
NSEvent* _lastMouseDownEvent; |
|||
bool _lastKeyHandled; |
|||
AvnPixelSize _lastPixelSize; |
|||
NSObject<IRenderTarget>* _renderTarget; |
|||
AvnPlatformResizeReason _resizeReason; |
|||
AvnAccessibilityElement* _accessibilityChild; |
|||
} |
|||
|
|||
- (void)onClosed |
|||
{ |
|||
@synchronized (self) |
|||
{ |
|||
_parent = nullptr; |
|||
} |
|||
} |
|||
|
|||
- (NSEvent*) lastMouseDownEvent |
|||
{ |
|||
return _lastMouseDownEvent; |
|||
} |
|||
|
|||
- (void) updateRenderTarget |
|||
{ |
|||
[_renderTarget resize:_lastPixelSize withScale:static_cast<float>([[self window] backingScaleFactor])]; |
|||
[self setNeedsDisplayInRect:[self frame]]; |
|||
} |
|||
|
|||
-(AvnView*) initWithParent: (WindowBaseImpl*) parent |
|||
{ |
|||
self = [super init]; |
|||
_renderTarget = parent->renderTarget; |
|||
[self setWantsLayer:YES]; |
|||
[self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize]; |
|||
|
|||
_parent = parent; |
|||
_area = nullptr; |
|||
_lastPixelSize.Height = 100; |
|||
_lastPixelSize.Width = 100; |
|||
[self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; |
|||
|
|||
_modifierState = AvnInputModifiersNone; |
|||
return self; |
|||
} |
|||
|
|||
- (BOOL)isFlipped |
|||
{ |
|||
return YES; |
|||
} |
|||
|
|||
- (BOOL)wantsUpdateLayer |
|||
{ |
|||
return YES; |
|||
} |
|||
|
|||
- (void)setLayer:(CALayer *)layer |
|||
{ |
|||
[_renderTarget setNewLayer: layer]; |
|||
[super setLayer: layer]; |
|||
} |
|||
|
|||
- (BOOL)isOpaque |
|||
{ |
|||
return YES; |
|||
} |
|||
|
|||
- (BOOL)acceptsFirstResponder |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
- (BOOL)acceptsFirstMouse:(NSEvent *)event |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
- (BOOL)canBecomeKeyView |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
-(void)setFrameSize:(NSSize)newSize |
|||
{ |
|||
[super setFrameSize:newSize]; |
|||
|
|||
if(_area != nullptr) |
|||
{ |
|||
[self removeTrackingArea:_area]; |
|||
_area = nullptr; |
|||
} |
|||
|
|||
if (_parent == nullptr) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
NSRect rect = NSZeroRect; |
|||
rect.size = newSize; |
|||
|
|||
NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingEnabledDuringMouseDrag; |
|||
_area = [[NSTrackingArea alloc] initWithRect:rect options:options owner:self userInfo:nullptr]; |
|||
[self addTrackingArea:_area]; |
|||
|
|||
_parent->UpdateCursor(); |
|||
|
|||
auto fsize = [self convertSizeToBacking: [self frame].size]; |
|||
|
|||
if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height) |
|||
{ |
|||
_lastPixelSize.Width = (int)fsize.width; |
|||
_lastPixelSize.Height = (int)fsize.height; |
|||
[self updateRenderTarget]; |
|||
|
|||
auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; |
|||
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); |
|||
} |
|||
} |
|||
|
|||
- (void)updateLayer |
|||
{ |
|||
AvnInsidePotentialDeadlock deadlock; |
|||
if (_parent == nullptr) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_parent->BaseEvents->RunRenderPriorityJobs(); |
|||
|
|||
if (_parent == nullptr) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_parent->BaseEvents->Paint(); |
|||
} |
|||
|
|||
- (void)drawRect:(NSRect)dirtyRect |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
-(void) setSwRenderedFrame: (AvnFramebuffer*) fb dispose: (IUnknown*) dispose |
|||
{ |
|||
@autoreleasepool { |
|||
[_renderTarget setSwFrame:fb]; |
|||
dispose->Release(); |
|||
} |
|||
} |
|||
|
|||
- (AvnPoint) translateLocalPoint:(AvnPoint)pt |
|||
{ |
|||
pt.Y = [self bounds].size.height - pt.Y; |
|||
return pt; |
|||
} |
|||
|
|||
+ (AvnPoint)toAvnPoint:(CGPoint)p |
|||
{ |
|||
AvnPoint result; |
|||
|
|||
result.X = p.x; |
|||
result.Y = p.y; |
|||
|
|||
return result; |
|||
} |
|||
|
|||
- (void) viewDidChangeBackingProperties |
|||
{ |
|||
auto fsize = [self convertSizeToBacking: [self frame].size]; |
|||
_lastPixelSize.Width = (int)fsize.width; |
|||
_lastPixelSize.Height = (int)fsize.height; |
|||
[self updateRenderTarget]; |
|||
|
|||
if(_parent != nullptr) |
|||
{ |
|||
_parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); |
|||
} |
|||
|
|||
[super viewDidChangeBackingProperties]; |
|||
} |
|||
|
|||
- (bool) ignoreUserInput:(bool)trigerInputWhenDisabled |
|||
{ |
|||
if(_parent == nullptr) |
|||
{ |
|||
return TRUE; |
|||
} |
|||
|
|||
auto parentWindow = _parent->GetWindowProtocol(); |
|||
|
|||
if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) |
|||
{ |
|||
if(trigerInputWhenDisabled) |
|||
{ |
|||
auto window = dynamic_cast<WindowImpl*>(_parent.getRaw()); |
|||
|
|||
if(window != nullptr) |
|||
{ |
|||
window->WindowEvents->GotInputWhenDisabled(); |
|||
} |
|||
} |
|||
|
|||
return TRUE; |
|||
} |
|||
|
|||
return FALSE; |
|||
} |
|||
|
|||
- (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type |
|||
{ |
|||
bool triggerInputWhenDisabled = type != Move; |
|||
|
|||
if([self ignoreUserInput: triggerInputWhenDisabled]) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; |
|||
auto avnPoint = [AvnView toAvnPoint:localPoint]; |
|||
auto point = [self translateLocalPoint:avnPoint]; |
|||
AvnVector delta = { 0, 0}; |
|||
|
|||
if(type == Wheel) |
|||
{ |
|||
auto speed = 5; |
|||
|
|||
if([event hasPreciseScrollingDeltas]) |
|||
{ |
|||
speed = 50; |
|||
} |
|||
|
|||
delta.X = [event scrollingDeltaX] / speed; |
|||
delta.Y = [event scrollingDeltaY] / speed; |
|||
|
|||
if(delta.X == 0 && delta.Y == 0) |
|||
{ |
|||
return; |
|||
} |
|||
} |
|||
else if (type == Magnify) |
|||
{ |
|||
delta.X = delta.Y = [event magnification]; |
|||
} |
|||
else if (type == Rotate) |
|||
{ |
|||
delta.X = delta.Y = [event rotation]; |
|||
} |
|||
else if (type == Swipe) |
|||
{ |
|||
delta.X = [event deltaX]; |
|||
delta.Y = [event deltaY]; |
|||
} |
|||
|
|||
uint32 timestamp = static_cast<uint32>([event timestamp] * 1000); |
|||
auto modifiers = [self getModifiers:[event modifierFlags]]; |
|||
|
|||
if(type != Move || |
|||
( |
|||
[self window] != nil && |
|||
( |
|||
[[self window] firstResponder] == nil |
|||
|| ![[[self window] firstResponder] isKindOfClass: [NSView class]] |
|||
) |
|||
) |
|||
) |
|||
[self becomeFirstResponder]; |
|||
|
|||
if(_parent != nullptr) |
|||
{ |
|||
_parent->BaseEvents->RawMouseEvent(type, timestamp, modifiers, point, delta); |
|||
} |
|||
|
|||
[super mouseMoved:event]; |
|||
} |
|||
|
|||
- (BOOL) resignFirstResponder |
|||
{ |
|||
_parent->BaseEvents->LostFocus(); |
|||
return YES; |
|||
} |
|||
|
|||
- (void)mouseMoved:(NSEvent *)event |
|||
{ |
|||
[self mouseEvent:event withType:Move]; |
|||
} |
|||
|
|||
- (void)mouseDown:(NSEvent *)event |
|||
{ |
|||
_isLeftPressed = true; |
|||
_lastMouseDownEvent = event; |
|||
[self mouseEvent:event withType:LeftButtonDown]; |
|||
} |
|||
|
|||
- (void)otherMouseDown:(NSEvent *)event |
|||
{ |
|||
_lastMouseDownEvent = event; |
|||
|
|||
switch(event.buttonNumber) |
|||
{ |
|||
case 2: |
|||
case 3: |
|||
_isMiddlePressed = true; |
|||
[self mouseEvent:event withType:MiddleButtonDown]; |
|||
break; |
|||
case 4: |
|||
_isXButton1Pressed = true; |
|||
[self mouseEvent:event withType:XButton1Down]; |
|||
break; |
|||
case 5: |
|||
_isXButton2Pressed = true; |
|||
[self mouseEvent:event withType:XButton2Down]; |
|||
break; |
|||
|
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
- (void)rightMouseDown:(NSEvent *)event |
|||
{ |
|||
_isRightPressed = true; |
|||
_lastMouseDownEvent = event; |
|||
[self mouseEvent:event withType:RightButtonDown]; |
|||
} |
|||
|
|||
- (void)mouseUp:(NSEvent *)event |
|||
{ |
|||
_isLeftPressed = false; |
|||
[self mouseEvent:event withType:LeftButtonUp]; |
|||
} |
|||
|
|||
- (void)otherMouseUp:(NSEvent *)event |
|||
{ |
|||
switch(event.buttonNumber) |
|||
{ |
|||
case 2: |
|||
case 3: |
|||
_isMiddlePressed = false; |
|||
[self mouseEvent:event withType:MiddleButtonUp]; |
|||
break; |
|||
case 4: |
|||
_isXButton1Pressed = false; |
|||
[self mouseEvent:event withType:XButton1Up]; |
|||
break; |
|||
case 5: |
|||
_isXButton2Pressed = false; |
|||
[self mouseEvent:event withType:XButton2Up]; |
|||
break; |
|||
|
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
- (void)rightMouseUp:(NSEvent *)event |
|||
{ |
|||
_isRightPressed = false; |
|||
[self mouseEvent:event withType:RightButtonUp]; |
|||
} |
|||
|
|||
- (void)mouseDragged:(NSEvent *)event |
|||
{ |
|||
[self mouseEvent:event withType:Move]; |
|||
[super mouseDragged:event]; |
|||
} |
|||
|
|||
- (void)otherMouseDragged:(NSEvent *)event |
|||
{ |
|||
[self mouseEvent:event withType:Move]; |
|||
[super otherMouseDragged:event]; |
|||
} |
|||
|
|||
- (void)rightMouseDragged:(NSEvent *)event |
|||
{ |
|||
[self mouseEvent:event withType:Move]; |
|||
[super rightMouseDragged:event]; |
|||
} |
|||
|
|||
- (void)scrollWheel:(NSEvent *)event |
|||
{ |
|||
[self mouseEvent:event withType:Wheel]; |
|||
[super scrollWheel:event]; |
|||
} |
|||
|
|||
- (void)magnifyWithEvent:(NSEvent *)event |
|||
{ |
|||
[self mouseEvent:event withType:Magnify]; |
|||
[super magnifyWithEvent:event]; |
|||
} |
|||
|
|||
- (void)rotateWithEvent:(NSEvent *)event |
|||
{ |
|||
[self mouseEvent:event withType:Rotate]; |
|||
[super rotateWithEvent:event]; |
|||
} |
|||
|
|||
- (void)swipeWithEvent:(NSEvent *)event |
|||
{ |
|||
[self mouseEvent:event withType:Swipe]; |
|||
[super swipeWithEvent:event]; |
|||
} |
|||
|
|||
- (void)mouseEntered:(NSEvent *)event |
|||
{ |
|||
[super mouseEntered:event]; |
|||
} |
|||
|
|||
- (void)mouseExited:(NSEvent *)event |
|||
{ |
|||
[self mouseEvent:event withType:LeaveWindow]; |
|||
[super mouseExited:event]; |
|||
} |
|||
|
|||
- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type |
|||
{ |
|||
if([self ignoreUserInput: false]) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
auto key = s_KeyMap[[event keyCode]]; |
|||
|
|||
uint32_t timestamp = static_cast<uint32_t>([event timestamp] * 1000); |
|||
auto modifiers = [self getModifiers:[event modifierFlags]]; |
|||
|
|||
if(_parent != nullptr) |
|||
{ |
|||
_lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); |
|||
} |
|||
} |
|||
|
|||
- (BOOL)performKeyEquivalent:(NSEvent *)event |
|||
{ |
|||
bool result = _lastKeyHandled; |
|||
|
|||
_lastKeyHandled = false; |
|||
|
|||
return result; |
|||
} |
|||
|
|||
- (void)flagsChanged:(NSEvent *)event |
|||
{ |
|||
auto newModifierState = [self getModifiers:[event modifierFlags]]; |
|||
|
|||
bool isAltCurrentlyPressed = (_modifierState & Alt) == Alt; |
|||
bool isControlCurrentlyPressed = (_modifierState & Control) == Control; |
|||
bool isShiftCurrentlyPressed = (_modifierState & Shift) == Shift; |
|||
bool isCommandCurrentlyPressed = (_modifierState & Windows) == Windows; |
|||
|
|||
bool isAltPressed = (newModifierState & Alt) == Alt; |
|||
bool isControlPressed = (newModifierState & Control) == Control; |
|||
bool isShiftPressed = (newModifierState & Shift) == Shift; |
|||
bool isCommandPressed = (newModifierState & Windows) == Windows; |
|||
|
|||
|
|||
if (isAltPressed && !isAltCurrentlyPressed) |
|||
{ |
|||
[self keyboardEvent:event withType:KeyDown]; |
|||
} |
|||
else if (isAltCurrentlyPressed && !isAltPressed) |
|||
{ |
|||
[self keyboardEvent:event withType:KeyUp]; |
|||
} |
|||
|
|||
if (isControlPressed && !isControlCurrentlyPressed) |
|||
{ |
|||
[self keyboardEvent:event withType:KeyDown]; |
|||
} |
|||
else if (isControlCurrentlyPressed && !isControlPressed) |
|||
{ |
|||
[self keyboardEvent:event withType:KeyUp]; |
|||
} |
|||
|
|||
if (isShiftPressed && !isShiftCurrentlyPressed) |
|||
{ |
|||
[self keyboardEvent:event withType:KeyDown]; |
|||
} |
|||
else if(isShiftCurrentlyPressed && !isShiftPressed) |
|||
{ |
|||
[self keyboardEvent:event withType:KeyUp]; |
|||
} |
|||
|
|||
if(isCommandPressed && !isCommandCurrentlyPressed) |
|||
{ |
|||
[self keyboardEvent:event withType:KeyDown]; |
|||
} |
|||
else if(isCommandCurrentlyPressed && ! isCommandPressed) |
|||
{ |
|||
[self keyboardEvent:event withType:KeyUp]; |
|||
} |
|||
|
|||
_modifierState = newModifierState; |
|||
|
|||
[[self inputContext] handleEvent:event]; |
|||
[super flagsChanged:event]; |
|||
} |
|||
|
|||
- (void)keyDown:(NSEvent *)event |
|||
{ |
|||
[self keyboardEvent:event withType:KeyDown]; |
|||
[[self inputContext] handleEvent:event]; |
|||
[super keyDown:event]; |
|||
} |
|||
|
|||
- (void)keyUp:(NSEvent *)event |
|||
{ |
|||
[self keyboardEvent:event withType:KeyUp]; |
|||
[super keyUp:event]; |
|||
} |
|||
|
|||
- (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod |
|||
{ |
|||
unsigned int rv = 0; |
|||
|
|||
if (mod & NSEventModifierFlagControl) |
|||
rv |= Control; |
|||
if (mod & NSEventModifierFlagShift) |
|||
rv |= Shift; |
|||
if (mod & NSEventModifierFlagOption) |
|||
rv |= Alt; |
|||
if (mod & NSEventModifierFlagCommand) |
|||
rv |= Windows; |
|||
|
|||
if (_isLeftPressed) |
|||
rv |= LeftMouseButton; |
|||
if (_isMiddlePressed) |
|||
rv |= MiddleMouseButton; |
|||
if (_isRightPressed) |
|||
rv |= RightMouseButton; |
|||
if (_isXButton1Pressed) |
|||
rv |= XButton1MouseButton; |
|||
if (_isXButton2Pressed) |
|||
rv |= XButton2MouseButton; |
|||
|
|||
return (AvnInputModifiers)rv; |
|||
} |
|||
|
|||
- (BOOL)hasMarkedText |
|||
{ |
|||
return _lastKeyHandled; |
|||
} |
|||
|
|||
- (NSRange)markedRange |
|||
{ |
|||
return NSMakeRange(NSNotFound, 0); |
|||
} |
|||
|
|||
- (NSRange)selectedRange |
|||
{ |
|||
return NSMakeRange(NSNotFound, 0); |
|||
} |
|||
|
|||
- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange |
|||
{ |
|||
|
|||
} |
|||
|
|||
- (void)unmarkText |
|||
{ |
|||
|
|||
} |
|||
|
|||
- (NSArray<NSString *> *)validAttributesForMarkedText |
|||
{ |
|||
return [NSArray new]; |
|||
} |
|||
|
|||
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange |
|||
{ |
|||
return [NSAttributedString new]; |
|||
} |
|||
|
|||
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange |
|||
{ |
|||
if(!_lastKeyHandled) |
|||
{ |
|||
if(_parent != nullptr) |
|||
{ |
|||
_lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
- (NSUInteger)characterIndexForPoint:(NSPoint)point |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange |
|||
{ |
|||
CGRect result = { 0 }; |
|||
|
|||
return result; |
|||
} |
|||
|
|||
- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)info |
|||
{ |
|||
auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; |
|||
auto avnPoint = [AvnView toAvnPoint:localPoint]; |
|||
auto point = [self translateLocalPoint:avnPoint]; |
|||
auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; |
|||
NSDragOperation nsop = [info draggingSourceOperationMask]; |
|||
|
|||
auto effects = ConvertDragDropEffects(nsop); |
|||
int reffects = (int)_parent->BaseEvents |
|||
->DragEvent(type, point, modifiers, effects, |
|||
CreateClipboard([info draggingPasteboard], nil), |
|||
GetAvnDataObjectHandleFromDraggingInfo(info)); |
|||
|
|||
NSDragOperation ret = static_cast<NSDragOperation>(0); |
|||
|
|||
// Ensure that the managed part didn't add any new effects |
|||
reffects = (int)effects & reffects; |
|||
|
|||
// OSX requires exactly one operation |
|||
if((reffects & (int)AvnDragDropEffects::Copy) != 0) |
|||
ret = NSDragOperationCopy; |
|||
else if((reffects & (int)AvnDragDropEffects::Move) != 0) |
|||
ret = NSDragOperationMove; |
|||
else if((reffects & (int)AvnDragDropEffects::Link) != 0) |
|||
ret = NSDragOperationLink; |
|||
if(ret == 0) |
|||
ret = NSDragOperationNone; |
|||
return ret; |
|||
} |
|||
|
|||
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender |
|||
{ |
|||
return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender]; |
|||
} |
|||
|
|||
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender |
|||
{ |
|||
return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender]; |
|||
} |
|||
|
|||
- (void)draggingExited:(id <NSDraggingInfo>)sender |
|||
{ |
|||
[self triggerAvnDragEvent: AvnDragEventType::Leave info:sender]; |
|||
} |
|||
|
|||
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender |
|||
{ |
|||
return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone; |
|||
} |
|||
|
|||
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender |
|||
{ |
|||
return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone; |
|||
} |
|||
|
|||
- (void)concludeDragOperation:(nullable id <NSDraggingInfo>)sender |
|||
{ |
|||
|
|||
} |
|||
|
|||
- (AvnPlatformResizeReason)getResizeReason |
|||
{ |
|||
return _resizeReason; |
|||
} |
|||
|
|||
- (void)setResizeReason:(AvnPlatformResizeReason)reason |
|||
{ |
|||
_resizeReason = reason; |
|||
} |
|||
|
|||
- (AvnAccessibilityElement *) accessibilityChild |
|||
{ |
|||
if (_accessibilityChild == nil) |
|||
{ |
|||
auto peer = _parent->BaseEvents->GetAutomationPeer(); |
|||
|
|||
if (peer == nil) |
|||
return nil; |
|||
|
|||
_accessibilityChild = [AvnAccessibilityElement acquire:peer]; |
|||
} |
|||
|
|||
return _accessibilityChild; |
|||
} |
|||
|
|||
- (NSArray *)accessibilityChildren |
|||
{ |
|||
auto child = [self accessibilityChild]; |
|||
return NSAccessibilityUnignoredChildrenForOnlyChild(child); |
|||
} |
|||
|
|||
- (id)accessibilityHitTest:(NSPoint)point |
|||
{ |
|||
return [[self accessibilityChild] accessibilityHitTest:point]; |
|||
} |
|||
|
|||
- (id)accessibilityFocusedUIElement |
|||
{ |
|||
return [[self accessibilityChild] accessibilityFocusedUIElement]; |
|||
} |
|||
|
|||
@end |
|||
@ -0,0 +1,441 @@ |
|||
// |
|||
// Created by Dan Walmsley on 06/05/2022. |
|||
// Copyright (c) 2022 Avalonia. All rights reserved. |
|||
// |
|||
|
|||
|
|||
#import <AppKit/AppKit.h> |
|||
#import "WindowProtocol.h" |
|||
#import "WindowBaseImpl.h" |
|||
|
|||
#ifdef IS_NSPANEL |
|||
#define BASE_CLASS NSPanel |
|||
#define CLASS_NAME AvnPanel |
|||
#else |
|||
#define BASE_CLASS NSWindow |
|||
#define CLASS_NAME AvnWindow |
|||
#endif |
|||
|
|||
#import <AppKit/AppKit.h> |
|||
#include "common.h" |
|||
#include "menu.h" |
|||
#include "automation.h" |
|||
#include "WindowBaseImpl.h" |
|||
#include "WindowImpl.h" |
|||
#include "AvnView.h" |
|||
#include "WindowInterfaces.h" |
|||
#include "PopupImpl.h" |
|||
|
|||
@implementation CLASS_NAME |
|||
{ |
|||
ComPtr<WindowBaseImpl> _parent; |
|||
bool _closed; |
|||
bool _isEnabled; |
|||
bool _isExtended; |
|||
AvnMenu* _menu; |
|||
} |
|||
|
|||
-(void) setIsExtended:(bool)value; |
|||
{ |
|||
_isExtended = value; |
|||
} |
|||
|
|||
-(bool) isDialog |
|||
{ |
|||
return _parent->IsDialog(); |
|||
} |
|||
|
|||
-(double) getExtendedTitleBarHeight |
|||
{ |
|||
if(_isExtended) |
|||
{ |
|||
for (id subview in self.contentView.superview.subviews) |
|||
{ |
|||
if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) |
|||
{ |
|||
NSView *titlebarView = [subview subviews][0]; |
|||
|
|||
return (double)titlebarView.frame.size.height; |
|||
} |
|||
} |
|||
|
|||
return -1; |
|||
} |
|||
else |
|||
{ |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
- (void)performClose:(id)sender |
|||
{ |
|||
if([[self delegate] respondsToSelector:@selector(windowShouldClose:)]) |
|||
{ |
|||
if(![[self delegate] windowShouldClose:self]) return; |
|||
} |
|||
else if([self respondsToSelector:@selector(windowShouldClose:)]) |
|||
{ |
|||
if(![self windowShouldClose:self]) return; |
|||
} |
|||
|
|||
[self close]; |
|||
} |
|||
|
|||
- (void)pollModalSession:(nonnull NSModalSession)session |
|||
{ |
|||
auto response = [NSApp runModalSession:session]; |
|||
|
|||
if(response == NSModalResponseContinue) |
|||
{ |
|||
dispatch_async(dispatch_get_main_queue(), ^{ |
|||
[self pollModalSession:session]; |
|||
}); |
|||
} |
|||
else if (!_closed) |
|||
{ |
|||
[self orderOut:self]; |
|||
[NSApp endModalSession:session]; |
|||
} |
|||
} |
|||
|
|||
-(void) showWindowMenuWithAppMenu |
|||
{ |
|||
if(_menu != nullptr) |
|||
{ |
|||
auto appMenuItem = ::GetAppMenuItem(); |
|||
|
|||
if(appMenuItem != nullptr) |
|||
{ |
|||
auto appMenu = [appMenuItem menu]; |
|||
|
|||
[appMenu removeItem:appMenuItem]; |
|||
|
|||
[_menu insertItem:appMenuItem atIndex:0]; |
|||
|
|||
[_menu setHasGlobalMenuItem:true]; |
|||
} |
|||
|
|||
[NSApp setMenu:_menu]; |
|||
} |
|||
else |
|||
{ |
|||
[self showAppMenuOnly]; |
|||
} |
|||
} |
|||
|
|||
-(void) showAppMenuOnly |
|||
{ |
|||
auto appMenuItem = ::GetAppMenuItem(); |
|||
|
|||
if(appMenuItem != nullptr) |
|||
{ |
|||
auto appMenu = ::GetAppMenu(); |
|||
|
|||
auto nativeAppMenu = dynamic_cast<AvnAppMenu*>(appMenu); |
|||
|
|||
[[appMenuItem menu] removeItem:appMenuItem]; |
|||
|
|||
if(_menu != nullptr) |
|||
{ |
|||
[_menu setHasGlobalMenuItem:false]; |
|||
} |
|||
|
|||
[nativeAppMenu->GetNative() addItem:appMenuItem]; |
|||
|
|||
[NSApp setMenu:nativeAppMenu->GetNative()]; |
|||
} |
|||
} |
|||
|
|||
-(void) applyMenu:(AvnMenu *)menu |
|||
{ |
|||
if(menu == nullptr) |
|||
{ |
|||
menu = [AvnMenu new]; |
|||
} |
|||
|
|||
_menu = menu; |
|||
} |
|||
|
|||
-(CLASS_NAME*) initWithParent: (WindowBaseImpl*) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; |
|||
{ |
|||
// https://jameshfisher.com/2020/07/10/why-is-the-contentrect-of-my-nswindow-ignored/ |
|||
// create nswindow with specific contentRect, otherwise we wont be able to resize the window |
|||
// until several ms after the window is physically on the screen. |
|||
self = [super initWithContentRect:contentRect styleMask: styleMask backing:NSBackingStoreBuffered defer:false]; |
|||
|
|||
[self setReleasedWhenClosed:false]; |
|||
_parent = parent; |
|||
[self setDelegate:self]; |
|||
_closed = false; |
|||
_isEnabled = true; |
|||
|
|||
[self backingScaleFactor]; |
|||
[self setOpaque:NO]; |
|||
[self setBackgroundColor: [NSColor clearColor]]; |
|||
|
|||
_isExtended = false; |
|||
return self; |
|||
} |
|||
|
|||
- (BOOL)windowShouldClose:(NSWindow *)sender |
|||
{ |
|||
auto window = dynamic_cast<WindowImpl*>(_parent.getRaw()); |
|||
|
|||
if(window != nullptr) |
|||
{ |
|||
return !window->WindowEvents->Closing(); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
- (void)windowDidChangeBackingProperties:(NSNotification *)notification |
|||
{ |
|||
[self backingScaleFactor]; |
|||
} |
|||
|
|||
- (void)windowWillClose:(NSNotification *)notification |
|||
{ |
|||
_closed = true; |
|||
if(_parent) |
|||
{ |
|||
ComPtr<WindowBaseImpl> parent = _parent; |
|||
_parent = NULL; |
|||
[self restoreParentWindow]; |
|||
parent->BaseEvents->Closed(); |
|||
[parent->View onClosed]; |
|||
} |
|||
} |
|||
|
|||
-(BOOL)canBecomeKeyWindow |
|||
{ |
|||
// If the window has a child window being shown as a dialog then don't allow it to become the key window. |
|||
for(NSWindow* uch in [self childWindows]) |
|||
{ |
|||
auto ch = static_cast<id <AvnWindowProtocol>>(uch); |
|||
if(ch == nil) |
|||
continue; |
|||
if (ch.isDialog) |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
-(BOOL)canBecomeMainWindow |
|||
{ |
|||
#ifdef IS_NSPANEL |
|||
return false; |
|||
#else |
|||
return true; |
|||
#endif |
|||
} |
|||
|
|||
-(bool)shouldTryToHandleEvents |
|||
{ |
|||
return _isEnabled; |
|||
} |
|||
|
|||
-(void) setEnabled:(bool)enable |
|||
{ |
|||
_isEnabled = enable; |
|||
} |
|||
|
|||
-(void)becomeKeyWindow |
|||
{ |
|||
[self showWindowMenuWithAppMenu]; |
|||
|
|||
if(_parent != nullptr) |
|||
{ |
|||
_parent->BaseEvents->Activated(); |
|||
} |
|||
|
|||
[super becomeKeyWindow]; |
|||
} |
|||
|
|||
-(void) restoreParentWindow; |
|||
{ |
|||
auto parent = [self parentWindow]; |
|||
|
|||
if(parent != nil) |
|||
{ |
|||
[parent removeChildWindow:self]; |
|||
} |
|||
} |
|||
|
|||
- (void)windowDidMiniaturize:(NSNotification *)notification |
|||
{ |
|||
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->()); |
|||
|
|||
if(parent != nullptr) |
|||
{ |
|||
parent->WindowStateChanged(); |
|||
} |
|||
} |
|||
|
|||
- (void)windowDidDeminiaturize:(NSNotification *)notification |
|||
{ |
|||
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->()); |
|||
|
|||
if(parent != nullptr) |
|||
{ |
|||
parent->WindowStateChanged(); |
|||
} |
|||
} |
|||
|
|||
- (void)windowDidResize:(NSNotification *)notification |
|||
{ |
|||
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->()); |
|||
|
|||
if(parent != nullptr) |
|||
{ |
|||
parent->WindowStateChanged(); |
|||
} |
|||
} |
|||
|
|||
- (void)windowWillExitFullScreen:(NSNotification *)notification |
|||
{ |
|||
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->()); |
|||
|
|||
if(parent != nullptr) |
|||
{ |
|||
parent->StartStateTransition(); |
|||
} |
|||
} |
|||
|
|||
- (void)windowDidExitFullScreen:(NSNotification *)notification |
|||
{ |
|||
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->()); |
|||
|
|||
if(parent != nullptr) |
|||
{ |
|||
parent->EndStateTransition(); |
|||
|
|||
if(parent->Decorations() != SystemDecorationsFull && parent->WindowState() == Maximized) |
|||
{ |
|||
NSRect screenRect = [[self screen] visibleFrame]; |
|||
[self setFrame:screenRect display:YES]; |
|||
} |
|||
|
|||
if(parent->WindowState() == Minimized) |
|||
{ |
|||
[self miniaturize:nullptr]; |
|||
} |
|||
|
|||
parent->WindowStateChanged(); |
|||
} |
|||
} |
|||
|
|||
- (void)windowWillEnterFullScreen:(NSNotification *)notification |
|||
{ |
|||
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->()); |
|||
|
|||
if(parent != nullptr) |
|||
{ |
|||
parent->StartStateTransition(); |
|||
} |
|||
} |
|||
|
|||
- (void)windowDidEnterFullScreen:(NSNotification *)notification |
|||
{ |
|||
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->()); |
|||
|
|||
if(parent != nullptr) |
|||
{ |
|||
parent->EndStateTransition(); |
|||
parent->WindowStateChanged(); |
|||
} |
|||
} |
|||
|
|||
- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
-(void)resignKeyWindow |
|||
{ |
|||
if(_parent) |
|||
_parent->BaseEvents->Deactivated(); |
|||
|
|||
[self showAppMenuOnly]; |
|||
|
|||
[super resignKeyWindow]; |
|||
} |
|||
|
|||
- (void)windowDidMove:(NSNotification *)notification |
|||
{ |
|||
AvnPoint position; |
|||
|
|||
if(_parent != nullptr) |
|||
{ |
|||
auto cparent = dynamic_cast<WindowImpl*>(_parent.getRaw()); |
|||
|
|||
if(cparent != nullptr) |
|||
{ |
|||
if(cparent->WindowState() == Maximized) |
|||
{ |
|||
cparent->SetWindowState(Normal); |
|||
} |
|||
} |
|||
|
|||
_parent->GetPosition(&position); |
|||
_parent->BaseEvents->PositionChanged(position); |
|||
} |
|||
} |
|||
|
|||
- (AvnPoint) translateLocalPoint:(AvnPoint)pt |
|||
{ |
|||
pt.Y = [self frame].size.height - pt.Y; |
|||
return pt; |
|||
} |
|||
|
|||
- (void)sendEvent:(NSEvent *)event |
|||
{ |
|||
[super sendEvent:event]; |
|||
|
|||
/// This is to detect non-client clicks. This can only be done on Windows... not popups, hence the dynamic_cast. |
|||
if(_parent != nullptr && dynamic_cast<WindowImpl*>(_parent.getRaw()) != nullptr) |
|||
{ |
|||
switch(event.type) |
|||
{ |
|||
case NSEventTypeLeftMouseDown: |
|||
{ |
|||
AvnView* view = _parent->View; |
|||
NSPoint windowPoint = [event locationInWindow]; |
|||
NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil]; |
|||
|
|||
if (!NSPointInRect(viewPoint, view.bounds)) |
|||
{ |
|||
auto avnPoint = [AvnView toAvnPoint:windowPoint]; |
|||
auto point = [self translateLocalPoint:avnPoint]; |
|||
AvnVector delta = { 0, 0 }; |
|||
|
|||
_parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, static_cast<uint32>([event timestamp] * 1000), AvnInputModifiersNone, point, delta); |
|||
} |
|||
} |
|||
break; |
|||
|
|||
case NSEventTypeMouseEntered: |
|||
{ |
|||
_parent->UpdateCursor(); |
|||
} |
|||
break; |
|||
|
|||
case NSEventTypeMouseExited: |
|||
{ |
|||
[[NSCursor arrowCursor] set]; |
|||
} |
|||
break; |
|||
|
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
- (void)disconnectParent { |
|||
_parent = nullptr; |
|||
} |
|||
|
|||
@end |
|||
|
|||
@ -0,0 +1,17 @@ |
|||
//
|
|||
// Created by Dan Walmsley on 04/05/2022.
|
|||
// Copyright (c) 2022 Avalonia. All rights reserved.
|
|||
//
|
|||
|
|||
#ifndef AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H |
|||
#define AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H |
|||
|
|||
@class AvnView; |
|||
|
|||
struct INSWindowHolder |
|||
{ |
|||
virtual NSWindow* _Nonnull GetNSWindow () = 0; |
|||
virtual NSView* _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,9 @@ |
|||
//
|
|||
// Created by Dan Walmsley on 06/05/2022.
|
|||
// Copyright (c) 2022 Avalonia. All rights reserved.
|
|||
//
|
|||
|
|||
#ifndef AVALONIA_NATIVE_OSX_POPUPIMPL_H |
|||
#define AVALONIA_NATIVE_OSX_POPUPIMPL_H |
|||
|
|||
#endif //AVALONIA_NATIVE_OSX_POPUPIMPL_H
|
|||
@ -0,0 +1,68 @@ |
|||
// |
|||
// Created by Dan Walmsley on 06/05/2022. |
|||
// Copyright (c) 2022 Avalonia. All rights reserved. |
|||
// |
|||
|
|||
#include "WindowInterfaces.h" |
|||
#include "AvnView.h" |
|||
#include "WindowImpl.h" |
|||
#include "automation.h" |
|||
#include "menu.h" |
|||
#include "common.h" |
|||
#import "WindowBaseImpl.h" |
|||
#import "WindowProtocol.h" |
|||
#import <AppKit/AppKit.h> |
|||
#include "PopupImpl.h" |
|||
|
|||
class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup |
|||
{ |
|||
private: |
|||
BEGIN_INTERFACE_MAP() |
|||
INHERIT_INTERFACE_MAP(WindowBaseImpl) |
|||
INTERFACE_MAP_ENTRY(IAvnPopup, IID_IAvnPopup) |
|||
END_INTERFACE_MAP() |
|||
virtual ~PopupImpl(){} |
|||
ComPtr<IAvnWindowEvents> WindowEvents; |
|||
PopupImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) |
|||
{ |
|||
WindowEvents = events; |
|||
[Window setLevel:NSPopUpMenuWindowLevel]; |
|||
} |
|||
protected: |
|||
virtual NSWindowStyleMask GetStyle() override |
|||
{ |
|||
return NSWindowStyleMaskBorderless; |
|||
} |
|||
|
|||
virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override |
|||
{ |
|||
START_COM_CALL; |
|||
|
|||
@autoreleasepool |
|||
{ |
|||
if (Window != nullptr) |
|||
{ |
|||
[Window setContentSize:NSSize{x, y}]; |
|||
|
|||
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; |
|||
} |
|||
|
|||
return S_OK; |
|||
} |
|||
} |
|||
public: |
|||
virtual bool ShouldTakeFocusOnShow() override |
|||
{ |
|||
return false; |
|||
} |
|||
}; |
|||
|
|||
|
|||
extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl) |
|||
{ |
|||
@autoreleasepool |
|||
{ |
|||
IAvnPopup* ptr = dynamic_cast<IAvnPopup*>(new PopupImpl(events, gl)); |
|||
return ptr; |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
//
|
|||
// 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 "avalonia-native.h" |
|||
|
|||
@class AvnView; |
|||
|
|||
class ResizeScope |
|||
{ |
|||
public: |
|||
ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason); |
|||
|
|||
~ResizeScope(); |
|||
private: |
|||
AvnView* _Nonnull _view; |
|||
AvnPlatformResizeReason _restore; |
|||
}; |
|||
|
|||
#endif //AVALONIA_NATIVE_OSX_RESIZESCOPE_H
|
|||
@ -0,0 +1,18 @@ |
|||
// |
|||
// Created by Dan Walmsley on 04/05/2022. |
|||
// Copyright (c) 2022 Avalonia. All rights reserved. |
|||
// |
|||
|
|||
#import <AppKit/AppKit.h> |
|||
#include "ResizeScope.h" |
|||
#include "AvnView.h" |
|||
|
|||
ResizeScope::ResizeScope(AvnView *view, AvnPlatformResizeReason reason) { |
|||
_view = view; |
|||
_restore = [view getResizeReason]; |
|||
[view setResizeReason:reason]; |
|||
} |
|||
|
|||
ResizeScope::~ResizeScope() { |
|||
[_view setResizeReason:_restore]; |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
//
|
|||
// 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 |
|||
|
|||
#include "rendertarget.h" |
|||
#include "INSWindowHolder.h" |
|||
|
|||
@class AutoFitContentView; |
|||
@class AvnMenu; |
|||
@protocol AvnWindowProtocol; |
|||
|
|||
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(); |
|||
|
|||
AutoFitContentView *StandardContainer; |
|||
AvnView *View; |
|||
NSWindow * Window; |
|||
ComPtr<IAvnWindowBaseEvents> BaseEvents; |
|||
ComPtr<IAvnGlContext> _glContext; |
|||
NSObject <IRenderTarget> *renderTarget; |
|||
AvnPoint lastPositionSet; |
|||
NSSize lastSize; |
|||
NSSize lastMinSize; |
|||
NSSize lastMaxSize; |
|||
AvnMenu* lastMenu; |
|||
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 NSWindow *GetNSWindow() override; |
|||
|
|||
virtual NSView *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(); |
|||
|
|||
id<AvnWindowProtocol> GetWindowProtocol (); |
|||
|
|||
protected: |
|||
virtual NSWindowStyleMask GetStyle(); |
|||
|
|||
void UpdateStyle(); |
|||
|
|||
private: |
|||
void CreateNSWindow (bool isDialog); |
|||
void CleanNSWindow (); |
|||
void InitialiseNSWindow (); |
|||
}; |
|||
|
|||
#endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H
|
|||
@ -0,0 +1,589 @@ |
|||
// |
|||
// Created by Dan Walmsley on 04/05/2022. |
|||
// Copyright (c) 2022 Avalonia. All rights reserved. |
|||
// |
|||
|
|||
#import <AppKit/AppKit.h> |
|||
#include "common.h" |
|||
#include "AvnView.h" |
|||
#include "menu.h" |
|||
#include "automation.h" |
|||
#include "cursor.h" |
|||
#include "ResizeScope.h" |
|||
#include "AutoFitContentView.h" |
|||
#import "WindowProtocol.h" |
|||
#import "WindowInterfaces.h" |
|||
#include "WindowBaseImpl.h" |
|||
|
|||
|
|||
WindowBaseImpl::~WindowBaseImpl() { |
|||
View = nullptr; |
|||
Window = nullptr; |
|||
} |
|||
|
|||
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]; |
|||
|
|||
lastPositionSet.X = 100; |
|||
lastPositionSet.Y = 100; |
|||
lastSize = NSSize { 100, 100 }; |
|||
lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; |
|||
lastMinSize = NSSize { 0, 0 }; |
|||
_lastTitle = @""; |
|||
|
|||
Window = nullptr; |
|||
lastMenu = nullptr; |
|||
} |
|||
|
|||
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; |
|||
} |
|||
|
|||
NSWindow *WindowBaseImpl::GetNSWindow() { |
|||
return Window; |
|||
} |
|||
|
|||
NSView *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 { |
|||
CreateNSWindow(isDialog); |
|||
InitialiseNSWindow(); |
|||
|
|||
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]; |
|||
|
|||
[GetWindowProtocol() 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 { |
|||
lastMinSize = ToNSSize(minSize); |
|||
lastMaxSize = ToNSSize(maxSize); |
|||
|
|||
if(Window != nullptr) { |
|||
[Window setContentMinSize:lastMinSize]; |
|||
[Window setContentMaxSize:lastMaxSize]; |
|||
} |
|||
|
|||
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 = lastMaxSize; |
|||
auto minSize = lastMinSize; |
|||
|
|||
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); |
|||
} |
|||
|
|||
lastSize = NSSize {x, y}; |
|||
|
|||
if(Window != nullptr) { |
|||
[Window setContentSize:lastSize]; |
|||
} |
|||
} |
|||
@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); |
|||
|
|||
lastMenu = nativeMenu->GetNative(); |
|||
|
|||
if(Window != nullptr) { |
|||
[GetWindowProtocol() applyMenu:lastMenu]; |
|||
|
|||
if ([Window isKeyWindow]) { |
|||
[GetWindowProtocol() 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()]; |
|||
} |
|||
|
|||
void WindowBaseImpl::CleanNSWindow() { |
|||
if(Window != nullptr) { |
|||
[GetWindowProtocol() disconnectParent]; |
|||
[Window close]; |
|||
Window = nullptr; |
|||
} |
|||
} |
|||
|
|||
void WindowBaseImpl::CreateNSWindow(bool isDialog) { |
|||
if (isDialog) { |
|||
if (![Window isKindOfClass:[AvnPanel class]]) { |
|||
CleanNSWindow(); |
|||
|
|||
Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; |
|||
} |
|||
} else { |
|||
if (![Window isKindOfClass:[AvnWindow class]]) { |
|||
CleanNSWindow(); |
|||
|
|||
Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; |
|||
} |
|||
} |
|||
} |
|||
|
|||
void WindowBaseImpl::InitialiseNSWindow() { |
|||
if(Window != nullptr) { |
|||
[Window setContentView:StandardContainer]; |
|||
[Window setStyleMask:NSWindowStyleMaskBorderless]; |
|||
[Window setBackingType:NSBackingStoreBuffered]; |
|||
|
|||
[Window setContentSize:lastSize]; |
|||
[Window setContentMinSize:lastMinSize]; |
|||
[Window setContentMaxSize:lastMaxSize]; |
|||
|
|||
[Window setOpaque:false]; |
|||
|
|||
if (lastMenu != nullptr) { |
|||
[GetWindowProtocol() applyMenu:lastMenu]; |
|||
|
|||
if ([Window isKeyWindow]) { |
|||
[GetWindowProtocol() showWindowMenuWithAppMenu]; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
id <AvnWindowProtocol> WindowBaseImpl::GetWindowProtocol() { |
|||
if(Window == nullptr) |
|||
{ |
|||
return nullptr; |
|||
} |
|||
|
|||
return static_cast<id <AvnWindowProtocol>>(Window); |
|||
} |
|||
|
|||
extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) |
|||
{ |
|||
@autoreleasepool |
|||
{ |
|||
IAvnWindow* ptr = (IAvnWindow*)new WindowImpl(events, gl); |
|||
return ptr; |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
//
|
|||
// 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,552 @@ |
|||
// |
|||
// Created by Dan Walmsley on 04/05/2022. |
|||
// Copyright (c) 2022 Avalonia. All rights reserved. |
|||
// |
|||
|
|||
#import <AppKit/AppKit.h> |
|||
#include "AutoFitContentView.h" |
|||
#include "AvnView.h" |
|||
#include "automation.h" |
|||
#include "WindowProtocol.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 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; |
|||
|
|||
bool created = Window == nullptr; |
|||
|
|||
WindowBaseImpl::Show(activate, isDialog); |
|||
|
|||
if(created) |
|||
{ |
|||
if(_isClientAreaExtended) |
|||
{ |
|||
[GetWindowProtocol() setIsExtended:true]; |
|||
SetExtendClientArea(true); |
|||
} |
|||
} |
|||
|
|||
HideOrShowTrafficLights(); |
|||
|
|||
return SetWindowState(_lastWindowState); |
|||
} |
|||
} |
|||
|
|||
HRESULT WindowImpl::SetEnabled(bool enable) { |
|||
START_COM_CALL; |
|||
|
|||
@autoreleasepool { |
|||
[GetWindowProtocol() 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(Window != nullptr) { |
|||
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; |
|||
} |
|||
|
|||
[GetWindowProtocol() 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 = [GetWindowProtocol() 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 = this->_isDialog ? NSWindowStyleMaskUtilityWindow : 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; |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
//
|
|||
// Created by Dan Walmsley on 06/05/2022.
|
|||
// Copyright (c) 2022 Avalonia. All rights reserved.
|
|||
//
|
|||
|
|||
#import <Foundation/Foundation.h> |
|||
#import <AppKit/AppKit.h> |
|||
#include "WindowProtocol.h" |
|||
#include "WindowBaseImpl.h" |
|||
|
|||
@interface AvnWindow : NSWindow <AvnWindowProtocol, NSWindowDelegate> |
|||
-(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; |
|||
@end |
|||
|
|||
@interface AvnPanel : NSPanel <AvnWindowProtocol, NSWindowDelegate> |
|||
-(AvnPanel* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent contentRect: (NSRect)contentRect styleMask: (NSWindowStyleMask)styleMask; |
|||
@end |
|||
@ -0,0 +1,25 @@ |
|||
//
|
|||
// Created by Dan Walmsley on 06/05/2022.
|
|||
// Copyright (c) 2022 Avalonia. All rights reserved.
|
|||
//
|
|||
|
|||
#pragma once |
|||
|
|||
#import <AppKit/AppKit.h> |
|||
|
|||
@class AvnMenu; |
|||
|
|||
@protocol AvnWindowProtocol |
|||
-(void) pollModalSession: (NSModalSession _Nonnull) session; |
|||
-(void) restoreParentWindow; |
|||
-(bool) shouldTryToHandleEvents; |
|||
-(void) setEnabled: (bool) enable; |
|||
-(void) showAppMenuOnly; |
|||
-(void) showWindowMenuWithAppMenu; |
|||
-(void) applyMenu:(AvnMenu* _Nullable)menu; |
|||
|
|||
-(double) getExtendedTitleBarHeight; |
|||
-(void) setIsExtended:(bool)value; |
|||
-(void) disconnectParent; |
|||
-(bool) isDialog; |
|||
@end |
|||
@ -1,77 +0,0 @@ |
|||
#ifndef window_h |
|||
#define window_h |
|||
|
|||
class WindowBaseImpl; |
|||
|
|||
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination> |
|||
-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; |
|||
-(NSEvent* _Nonnull) lastMouseDownEvent; |
|||
-(AvnPoint) translateLocalPoint:(AvnPoint)pt; |
|||
-(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; |
|||
-(void) onClosed; |
|||
-(AvnPixelSize) getPixelSize; |
|||
-(AvnPlatformResizeReason) getResizeReason; |
|||
-(void) setResizeReason:(AvnPlatformResizeReason)reason; |
|||
+ (AvnPoint)toAvnPoint:(CGPoint)p; |
|||
@end |
|||
|
|||
@interface AutoFitContentView : NSView |
|||
-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; |
|||
-(void) ShowTitleBar: (bool) show; |
|||
-(void) SetTitleBarHeightHint: (double) height; |
|||
-(void) SetContent: (NSView* _Nonnull) content; |
|||
-(void) ShowBlur: (bool) show; |
|||
@end |
|||
|
|||
@interface AvnWindow : NSWindow <NSWindowDelegate> |
|||
+(void) closeAll; |
|||
-(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; |
|||
-(void) setCanBecomeKeyAndMain; |
|||
-(void) pollModalSession: (NSModalSession _Nonnull) session; |
|||
-(void) restoreParentWindow; |
|||
-(bool) shouldTryToHandleEvents; |
|||
-(void) setEnabled: (bool) enable; |
|||
-(void) showAppMenuOnly; |
|||
-(void) showWindowMenuWithAppMenu; |
|||
-(void) applyMenu:(NSMenu* _Nullable)menu; |
|||
-(double) getScaling; |
|||
-(double) getExtendedTitleBarHeight; |
|||
-(void) setIsExtended:(bool)value; |
|||
-(bool) isDialog; |
|||
@end |
|||
|
|||
struct INSWindowHolder |
|||
{ |
|||
virtual AvnWindow* _Nonnull GetNSWindow () = 0; |
|||
virtual AvnView* _Nonnull GetNSView () = 0; |
|||
}; |
|||
|
|||
struct IWindowStateChanged |
|||
{ |
|||
virtual void WindowStateChanged () = 0; |
|||
virtual void StartStateTransition () = 0; |
|||
virtual void EndStateTransition () = 0; |
|||
virtual SystemDecorations Decorations () = 0; |
|||
virtual AvnWindowState WindowState () = 0; |
|||
}; |
|||
|
|||
class ResizeScope |
|||
{ |
|||
public: |
|||
ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason) |
|||
{ |
|||
_view = view; |
|||
_restore = [view getResizeReason]; |
|||
[view setResizeReason:reason]; |
|||
} |
|||
|
|||
~ResizeScope() |
|||
{ |
|||
[_view setResizeReason:_restore]; |
|||
} |
|||
private: |
|||
AvnView* _Nonnull _view; |
|||
AvnPlatformResizeReason _restore; |
|||
}; |
|||
|
|||
#endif /* window_h */ |
|||
File diff suppressed because it is too large
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -1,414 +0,0 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under the MIT License.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
internal static class ColorHelpers |
|||
{ |
|||
public const int CheckerSize = 4; |
|||
|
|||
public static bool ToDisplayNameExists |
|||
{ |
|||
get => false; |
|||
} |
|||
|
|||
public static string ToDisplayName(Color color) |
|||
{ |
|||
return string.Empty; |
|||
} |
|||
|
|||
public static Hsv IncrementColorComponent( |
|||
Hsv originalHsv, |
|||
HsvComponent component, |
|||
IncrementDirection direction, |
|||
IncrementAmount amount, |
|||
bool shouldWrap, |
|||
double minBound, |
|||
double maxBound) |
|||
{ |
|||
Hsv newHsv = originalHsv; |
|||
|
|||
if (amount == IncrementAmount.Small || !ToDisplayNameExists) |
|||
{ |
|||
// In order to avoid working with small values that can incur rounding issues,
|
|||
// we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1.
|
|||
newHsv.S *= 100; |
|||
newHsv.V *= 100; |
|||
|
|||
// Note: *valueToIncrement replaced with ref local variable for C#, must be initialized
|
|||
ref double valueToIncrement = ref newHsv.H; |
|||
double incrementAmount = 0.0; |
|||
|
|||
// If we're adding a small increment, then we'll just add or subtract 1.
|
|||
// If we're adding a large increment, then we want to snap to the next
|
|||
// or previous major value - for hue, this is every increment of 30;
|
|||
// for saturation and value, this is every increment of 10.
|
|||
switch (component) |
|||
{ |
|||
case HsvComponent.Hue: |
|||
valueToIncrement = ref newHsv.H; |
|||
incrementAmount = amount == IncrementAmount.Small ? 1 : 30; |
|||
break; |
|||
|
|||
case HsvComponent.Saturation: |
|||
valueToIncrement = ref newHsv.S; |
|||
incrementAmount = amount == IncrementAmount.Small ? 1 : 10; |
|||
break; |
|||
|
|||
case HsvComponent.Value: |
|||
valueToIncrement = ref newHsv.V; |
|||
incrementAmount = amount == IncrementAmount.Small ? 1 : 10; |
|||
break; |
|||
|
|||
default: |
|||
throw new InvalidOperationException("Invalid HsvComponent."); |
|||
} |
|||
|
|||
double previousValue = valueToIncrement; |
|||
|
|||
valueToIncrement += (direction == IncrementDirection.Lower ? -incrementAmount : incrementAmount); |
|||
|
|||
// If the value has reached outside the bounds, we were previous at the boundary, and we should wrap,
|
|||
// then we'll place the selection on the other side of the spectrum.
|
|||
// Otherwise, we'll place it on the boundary that was exceeded.
|
|||
if (valueToIncrement < minBound) |
|||
{ |
|||
valueToIncrement = (shouldWrap && previousValue == minBound) ? maxBound : minBound; |
|||
} |
|||
|
|||
if (valueToIncrement > maxBound) |
|||
{ |
|||
valueToIncrement = (shouldWrap && previousValue == maxBound) ? minBound : maxBound; |
|||
} |
|||
|
|||
// We multiplied saturation and value by 100 previously, so now we want to put them back in the 0-1 range.
|
|||
newHsv.S /= 100; |
|||
newHsv.V /= 100; |
|||
} |
|||
else |
|||
{ |
|||
// While working with named colors, we're going to need to be working in actual HSV units,
|
|||
// so we'll divide the min bound and max bound by 100 in the case of saturation or value,
|
|||
// since we'll have received units between 0-100 and we need them within 0-1.
|
|||
if (component == HsvComponent.Saturation || |
|||
component == HsvComponent.Value) |
|||
{ |
|||
minBound /= 100; |
|||
maxBound /= 100; |
|||
} |
|||
|
|||
newHsv = FindNextNamedColor(originalHsv, component, direction, shouldWrap, minBound, maxBound); |
|||
} |
|||
|
|||
return newHsv; |
|||
} |
|||
|
|||
public static Hsv FindNextNamedColor( |
|||
Hsv originalHsv, |
|||
HsvComponent component, |
|||
IncrementDirection direction, |
|||
bool shouldWrap, |
|||
double minBound, |
|||
double maxBound) |
|||
{ |
|||
// There's no easy way to directly get the next named color, so what we'll do
|
|||
// is just iterate in the direction that we want to find it until we find a color
|
|||
// in that direction that has a color name different than our current color name.
|
|||
// Once we find a new color name, then we'll iterate across that color name until
|
|||
// we find its bounds on the other side, and then select the color that is exactly
|
|||
// in the middle of that color's bounds.
|
|||
Hsv newHsv = originalHsv; |
|||
|
|||
string originalColorName = ColorHelpers.ToDisplayName(originalHsv.ToRgb().ToColor()); |
|||
string newColorName = originalColorName; |
|||
|
|||
// Note: *newValue replaced with ref local variable for C#, must be initialized
|
|||
double originalValue = 0.0; |
|||
ref double newValue = ref newHsv.H; |
|||
double incrementAmount = 0.0; |
|||
|
|||
switch (component) |
|||
{ |
|||
case HsvComponent.Hue: |
|||
originalValue = originalHsv.H; |
|||
newValue = ref newHsv.H; |
|||
incrementAmount = 1; |
|||
break; |
|||
|
|||
case HsvComponent.Saturation: |
|||
originalValue = originalHsv.S; |
|||
newValue = ref newHsv.S; |
|||
incrementAmount = 0.01; |
|||
break; |
|||
|
|||
case HsvComponent.Value: |
|||
originalValue = originalHsv.V; |
|||
newValue = ref newHsv.V; |
|||
incrementAmount = 0.01; |
|||
break; |
|||
|
|||
default: |
|||
throw new InvalidOperationException("Invalid HsvComponent."); |
|||
} |
|||
|
|||
bool shouldFindMidPoint = true; |
|||
|
|||
while (newColorName == originalColorName) |
|||
{ |
|||
double previousValue = newValue; |
|||
newValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; |
|||
|
|||
bool justWrapped = false; |
|||
|
|||
// If we've hit a boundary, then either we should wrap or we shouldn't.
|
|||
// If we should, then we'll perform that wrapping if we were previously up against
|
|||
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
|
|||
if (newValue > maxBound) |
|||
{ |
|||
if (shouldWrap) |
|||
{ |
|||
newValue = minBound; |
|||
justWrapped = true; |
|||
} |
|||
else |
|||
{ |
|||
newValue = maxBound; |
|||
shouldFindMidPoint = false; |
|||
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); |
|||
break; |
|||
} |
|||
} |
|||
else if (newValue < minBound) |
|||
{ |
|||
if (shouldWrap) |
|||
{ |
|||
newValue = maxBound; |
|||
justWrapped = true; |
|||
} |
|||
else |
|||
{ |
|||
newValue = minBound; |
|||
shouldFindMidPoint = false; |
|||
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (!justWrapped && |
|||
previousValue != originalValue && |
|||
Math.Sign(newValue - originalValue) != Math.Sign(previousValue - originalValue)) |
|||
{ |
|||
// If we've wrapped all the way back to the start and have failed to find a new color name,
|
|||
// then we'll just quit - there isn't a new color name that we're going to find.
|
|||
shouldFindMidPoint = false; |
|||
break; |
|||
} |
|||
|
|||
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); |
|||
} |
|||
|
|||
if (shouldFindMidPoint) |
|||
{ |
|||
Hsv startHsv = newHsv; |
|||
Hsv currentHsv = startHsv; |
|||
double startEndOffset = 0; |
|||
string currentColorName = newColorName; |
|||
|
|||
// Note: *startValue/*currentValue replaced with ref local variables for C#, must be initialized
|
|||
ref double startValue = ref startHsv.H; |
|||
ref double currentValue = ref currentHsv.H; |
|||
double wrapIncrement = 0; |
|||
|
|||
switch (component) |
|||
{ |
|||
case HsvComponent.Hue: |
|||
startValue = ref startHsv.H; |
|||
currentValue = ref currentHsv.H; |
|||
wrapIncrement = 360.0; |
|||
break; |
|||
|
|||
case HsvComponent.Saturation: |
|||
startValue = ref startHsv.S; |
|||
currentValue = ref currentHsv.S; |
|||
wrapIncrement = 1.0; |
|||
break; |
|||
|
|||
case HsvComponent.Value: |
|||
startValue = ref startHsv.V; |
|||
currentValue = ref currentHsv.V; |
|||
wrapIncrement = 1.0; |
|||
break; |
|||
|
|||
default: |
|||
throw new InvalidOperationException("Invalid HsvComponent."); |
|||
} |
|||
|
|||
while (newColorName == currentColorName) |
|||
{ |
|||
currentValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; |
|||
|
|||
// If we've hit a boundary, then either we should wrap or we shouldn't.
|
|||
// If we should, then we'll perform that wrapping if we were previously up against
|
|||
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
|
|||
if (currentValue > maxBound) |
|||
{ |
|||
if (shouldWrap) |
|||
{ |
|||
currentValue = minBound; |
|||
startEndOffset = maxBound - minBound; |
|||
} |
|||
else |
|||
{ |
|||
currentValue = maxBound; |
|||
break; |
|||
} |
|||
} |
|||
else if (currentValue < minBound) |
|||
{ |
|||
if (shouldWrap) |
|||
{ |
|||
currentValue = maxBound; |
|||
startEndOffset = minBound - maxBound; |
|||
} |
|||
else |
|||
{ |
|||
currentValue = minBound; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
currentColorName = ColorHelpers.ToDisplayName(currentHsv.ToRgb().ToColor()); |
|||
} |
|||
|
|||
newValue = (startValue + currentValue + startEndOffset) / 2; |
|||
|
|||
// Dividing by 2 may have gotten us halfway through a single step, so we'll
|
|||
// remove that half-step if it exists.
|
|||
double leftoverValue = Math.Abs(newValue); |
|||
|
|||
while (leftoverValue > incrementAmount) |
|||
{ |
|||
leftoverValue -= incrementAmount; |
|||
} |
|||
|
|||
newValue -= leftoverValue; |
|||
|
|||
while (newValue < minBound) |
|||
{ |
|||
newValue += wrapIncrement; |
|||
} |
|||
|
|||
while (newValue > maxBound) |
|||
{ |
|||
newValue -= wrapIncrement; |
|||
} |
|||
} |
|||
|
|||
return newHsv; |
|||
} |
|||
|
|||
public static double IncrementAlphaComponent( |
|||
double originalAlpha, |
|||
IncrementDirection direction, |
|||
IncrementAmount amount, |
|||
bool shouldWrap, |
|||
double minBound, |
|||
double maxBound) |
|||
{ |
|||
// In order to avoid working with small values that can incur rounding issues,
|
|||
// we'll multiple alpha by 100 to put it in the range of 0-100 instead of 0-1.
|
|||
originalAlpha *= 100; |
|||
|
|||
const double smallIncrementAmount = 1; |
|||
const double largeIncrementAmount = 10; |
|||
|
|||
if (amount == IncrementAmount.Small) |
|||
{ |
|||
originalAlpha += (direction == IncrementDirection.Lower ? -1 : 1) * smallIncrementAmount; |
|||
} |
|||
else |
|||
{ |
|||
if (direction == IncrementDirection.Lower) |
|||
{ |
|||
originalAlpha = Math.Ceiling((originalAlpha - largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount; |
|||
} |
|||
else |
|||
{ |
|||
originalAlpha = Math.Floor((originalAlpha + largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount; |
|||
} |
|||
} |
|||
|
|||
// If the value has reached outside the bounds and we should wrap, then we'll place the selection
|
|||
// on the other side of the spectrum. Otherwise, we'll place it on the boundary that was exceeded.
|
|||
if (originalAlpha < minBound) |
|||
{ |
|||
originalAlpha = shouldWrap ? maxBound : minBound; |
|||
} |
|||
|
|||
if (originalAlpha > maxBound) |
|||
{ |
|||
originalAlpha = shouldWrap ? minBound : maxBound; |
|||
} |
|||
|
|||
// We multiplied alpha by 100 previously, so now we want to put it back in the 0-1 range.
|
|||
return originalAlpha / 100; |
|||
} |
|||
|
|||
public static WriteableBitmap CreateBitmapFromPixelData( |
|||
int pixelWidth, |
|||
int pixelHeight, |
|||
List<byte> bgraPixelData) |
|||
{ |
|||
Vector dpi = new Vector(96, 96); // Standard may need to change on some devices
|
|||
|
|||
WriteableBitmap bitmap = new WriteableBitmap( |
|||
new PixelSize(pixelWidth, pixelHeight), |
|||
dpi, |
|||
PixelFormat.Bgra8888, |
|||
AlphaFormat.Premul); |
|||
|
|||
// Warning: This is highly questionable
|
|||
using (var frameBuffer = bitmap.Lock()) |
|||
{ |
|||
Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); |
|||
} |
|||
|
|||
return bitmap; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the relative (perceptual) luminance/brightness of the given color.
|
|||
/// 1 is closer to white while 0 is closer to black.
|
|||
/// </summary>
|
|||
/// <param name="color">The color to calculate relative luminance for.</param>
|
|||
/// <returns>The relative (perceptual) luminance/brightness of the given color.</returns>
|
|||
public static double GetRelativeLuminance(Color color) |
|||
{ |
|||
// The equation for relative luminance is given by
|
|||
//
|
|||
// L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg
|
|||
//
|
|||
// where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise }
|
|||
//
|
|||
// If L is closer to 1, then the color is closer to white; if it is closer to 0,
|
|||
// then the color is closer to black. This is based on the fact that the human
|
|||
// eye perceives green to be much brighter than red, which in turn is perceived to be
|
|||
// brighter than blue.
|
|||
|
|||
double rg = color.R <= 10 ? color.R / 3294.0 : Math.Pow(color.R / 269.0 + 0.0513, 2.4); |
|||
double gg = color.G <= 10 ? color.G / 3294.0 : Math.Pow(color.G / 269.0 + 0.0513, 2.4); |
|||
double bg = color.B <= 10 ? color.B / 3294.0 : Math.Pow(color.B / 269.0 + 0.0513, 2.4); |
|||
|
|||
return (0.2126 * rg + 0.7152 * gg + 0.0722 * bg); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Data.Converters; |
|||
using Avalonia.Media; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Controls.Converters |
|||
{ |
|||
/// <summary>
|
|||
/// Converts the given value into a <see cref="Color"/> when a conversion is possible.
|
|||
/// </summary>
|
|||
public class ToColorConverter : IValueConverter |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public object? Convert( |
|||
object? value, |
|||
Type targetType, |
|||
object? parameter, |
|||
CultureInfo culture) |
|||
{ |
|||
if (value is Color valueColor) |
|||
{ |
|||
return valueColor; |
|||
} |
|||
else if (value is HslColor valueHslColor) |
|||
{ |
|||
return valueHslColor.ToRgb(); |
|||
} |
|||
else if (value is HsvColor valueHsvColor) |
|||
{ |
|||
return valueHsvColor.ToRgb(); |
|||
} |
|||
else if (value is SolidColorBrush valueBrush) |
|||
{ |
|||
// A brush may have an opacity set along with alpha transparency
|
|||
double alpha = valueBrush.Color.A * valueBrush.Opacity; |
|||
|
|||
return new Color( |
|||
(byte)MathUtilities.Clamp(alpha, 0x00, 0xFF), |
|||
valueBrush.Color.R, |
|||
valueBrush.Color.G, |
|||
valueBrush.Color.B); |
|||
} |
|||
|
|||
return AvaloniaProperty.UnsetValue; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public object? ConvertBack( |
|||
object? value, |
|||
Type targetType, |
|||
object? parameter, |
|||
CultureInfo culture) |
|||
{ |
|||
return AvaloniaProperty.UnsetValue; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using Avalonia.Data.Converters; |
|||
|
|||
namespace Avalonia.Controls.Primitives.Converters |
|||
{ |
|||
/// <summary>
|
|||
/// Converter to chain together multiple converters.
|
|||
/// </summary>
|
|||
public class ValueConverterGroup : List<IValueConverter>, IValueConverter |
|||
{ |
|||
/// <inheritdoc/>
|
|||
/// <inheritdoc/>
|
|||
public object? Convert( |
|||
object? value, |
|||
Type targetType, |
|||
object? parameter, |
|||
CultureInfo culture) |
|||
{ |
|||
object? curValue; |
|||
|
|||
curValue = value; |
|||
for (int i = 0; i < Count; i++) |
|||
{ |
|||
curValue = this[i].Convert(curValue, targetType, parameter, culture); |
|||
} |
|||
|
|||
return curValue; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public object? ConvertBack( |
|||
object? value, |
|||
Type targetType, |
|||
object? parameter, |
|||
CultureInfo culture) |
|||
{ |
|||
object? curValue; |
|||
|
|||
curValue = value; |
|||
for (int i = (Count - 1); i >= 0; i--) |
|||
{ |
|||
curValue = this[i].ConvertBack(curValue, targetType, parameter, culture); |
|||
} |
|||
|
|||
return curValue; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,142 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Media; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
/// <summary>
|
|||
/// Contains helpers useful when working with colors.
|
|||
/// </summary>
|
|||
public static class ColorHelper |
|||
{ |
|||
private static readonly Dictionary<Color, string> cachedDisplayNames = new Dictionary<Color, string>(); |
|||
private static readonly object cacheMutex = new object(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the relative (perceptual) luminance/brightness of the given color.
|
|||
/// 1 is closer to white while 0 is closer to black.
|
|||
/// </summary>
|
|||
/// <param name="color">The color to calculate relative luminance for.</param>
|
|||
/// <returns>The relative (perceptual) luminance/brightness of the given color.</returns>
|
|||
public static double GetRelativeLuminance(Color color) |
|||
{ |
|||
// The equation for relative luminance is given by
|
|||
//
|
|||
// L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg
|
|||
//
|
|||
// where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise }
|
|||
//
|
|||
// If L is closer to 1, then the color is closer to white; if it is closer to 0,
|
|||
// then the color is closer to black. This is based on the fact that the human
|
|||
// eye perceives green to be much brighter than red, which in turn is perceived to be
|
|||
// brighter than blue.
|
|||
|
|||
double rg = color.R <= 10 ? color.R / 3294.0 : Math.Pow(color.R / 269.0 + 0.0513, 2.4); |
|||
double gg = color.G <= 10 ? color.G / 3294.0 : Math.Pow(color.G / 269.0 + 0.0513, 2.4); |
|||
double bg = color.B <= 10 ? color.B / 3294.0 : Math.Pow(color.B / 269.0 + 0.0513, 2.4); |
|||
|
|||
return (0.2126 * rg + 0.7152 * gg + 0.0722 * bg); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Determines if color display names are supported based on the current thread culture.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Only English names are currently supported following known color names.
|
|||
/// In the future known color names could be localized.
|
|||
/// </remarks>
|
|||
public static bool ToDisplayNameExists |
|||
{ |
|||
get => CultureInfo.CurrentUICulture.Name.StartsWith("EN", StringComparison.OrdinalIgnoreCase); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Determines an approximate display name for the given color.
|
|||
/// </summary>
|
|||
/// <param name="color">The color to get the display name for.</param>
|
|||
/// <returns>The approximate color display name.</returns>
|
|||
public static string ToDisplayName(Color color) |
|||
{ |
|||
// Without rounding, there are 16,777,216 possible RGB colors (without alpha).
|
|||
// This is too many to cache and search through for performance reasons.
|
|||
// It is also needlessly large as there are only ~140 known/named colors.
|
|||
// Therefore, rounding of the input color's component values is done to
|
|||
// reduce the color space into something more useful.
|
|||
double rounding = 5; |
|||
var roundedColor = new Color( |
|||
0xFF, |
|||
Convert.ToByte(Math.Round(color.R / rounding) * rounding), |
|||
Convert.ToByte(Math.Round(color.G / rounding) * rounding), |
|||
Convert.ToByte(Math.Round(color.B / rounding) * rounding)); |
|||
|
|||
// Attempt to use a previously cached display name
|
|||
lock (cacheMutex) |
|||
{ |
|||
if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName)) |
|||
{ |
|||
return displayName; |
|||
} |
|||
} |
|||
|
|||
// Find the closest known color by measuring 3D Euclidean distance (ignore alpha)
|
|||
var closestKnownColor = KnownColor.None; |
|||
var closestKnownColorDistance = double.PositiveInfinity; |
|||
var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor)); |
|||
|
|||
for (int i = 1; i < knownColors.Length; i++) // Skip 'None'
|
|||
{ |
|||
// Transparent is skipped since alpha is ignored making it equivalent to White
|
|||
if (knownColors[i] != KnownColor.Transparent) |
|||
{ |
|||
Color knownColor = KnownColors.ToColor(knownColors[i]); |
|||
|
|||
double distance = Math.Sqrt( |
|||
Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) + |
|||
Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) + |
|||
Math.Pow((double)(roundedColor.B - knownColor.B), 2.0)); |
|||
|
|||
if (distance < closestKnownColorDistance) |
|||
{ |
|||
closestKnownColor = knownColors[i]; |
|||
closestKnownColorDistance = distance; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Return the closest known color as the display name
|
|||
// Cache results for next time as well
|
|||
if (closestKnownColor != KnownColor.None) |
|||
{ |
|||
StringBuilder sb = new StringBuilder(); |
|||
string name = closestKnownColor.ToString(); |
|||
|
|||
// Add spaces converting PascalCase to human-readable names
|
|||
for (int i = 0; i < name.Length; i++) |
|||
{ |
|||
if (i != 0 && |
|||
char.IsUpper(name[i])) |
|||
{ |
|||
sb.Append(' '); |
|||
} |
|||
|
|||
sb.Append(name[i]); |
|||
} |
|||
|
|||
string displayName = sb.ToString(); |
|||
|
|||
lock (cacheMutex) |
|||
{ |
|||
cachedDisplayNames.Add(roundedColor, displayName); |
|||
} |
|||
|
|||
return displayName; |
|||
} |
|||
else |
|||
{ |
|||
return string.Empty; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,629 @@ |
|||
// This source file is adapted from the WinUI project.
|
|||
// (https://github.com/microsoft/microsoft-ui-xaml)
|
|||
//
|
|||
// Licensed to The Avalonia Project under the MIT License.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Layout; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
/// <summary>
|
|||
/// Contains internal, special-purpose helpers used with the color picker.
|
|||
/// </summary>
|
|||
internal static class ColorPickerHelpers |
|||
{ |
|||
/// <summary>
|
|||
/// Generates a new bitmap of the specified size by changing a specific color component.
|
|||
/// This will produce a gradient representing a sweep of all possible values of the color component.
|
|||
/// </summary>
|
|||
/// <param name="width">The pixel width (X, horizontal) of the resulting bitmap.</param>
|
|||
/// <param name="height">The pixel height (Y, vertical) of the resulting bitmap.</param>
|
|||
/// <param name="orientation">The orientation of the resulting bitmap (gradient direction).</param>
|
|||
/// <param name="colorModel">The color model being used: RGBA or HSVA.</param>
|
|||
/// <param name="component">The specific color component to sweep.</param>
|
|||
/// <param name="baseHsvColor">The base HSV color used for components not being changed.</param>
|
|||
/// <param name="isAlphaMaxForced">Fix the alpha component value to maximum during calculation.
|
|||
/// This will remove any alpha/transparency from the other component backgrounds.</param>
|
|||
/// <param name="isSaturationValueMaxForced">Fix the saturation and value components to maximum
|
|||
/// during calculation with the HSVA color model.
|
|||
/// This will ensure colors are always discernible regardless of saturation/value.</param>
|
|||
/// <returns>A new bitmap representing a gradient of color component values.</returns>
|
|||
public static async Task<byte[]> CreateComponentBitmapAsync( |
|||
int width, |
|||
int height, |
|||
Orientation orientation, |
|||
ColorModel colorModel, |
|||
ColorComponent component, |
|||
HsvColor baseHsvColor, |
|||
bool isAlphaMaxForced, |
|||
bool isSaturationValueMaxForced) |
|||
{ |
|||
if (width == 0 || height == 0) |
|||
{ |
|||
return new byte[0]; |
|||
} |
|||
|
|||
var bitmap = await Task.Run<byte[]>(() => |
|||
{ |
|||
int pixelDataIndex = 0; |
|||
double componentStep; |
|||
byte[] bgraPixelData; |
|||
Color baseRgbColor = Colors.White; |
|||
Color rgbColor; |
|||
int bgraPixelDataHeight; |
|||
int bgraPixelDataWidth; |
|||
|
|||
// Allocate the buffer
|
|||
// BGRA formatted color components 1 byte each (4 bytes in a pixel)
|
|||
bgraPixelData = new byte[width * height * 4]; |
|||
bgraPixelDataHeight = height * 4; |
|||
bgraPixelDataWidth = width * 4; |
|||
|
|||
// Maximize alpha component value
|
|||
if (isAlphaMaxForced && |
|||
component != ColorComponent.Alpha) |
|||
{ |
|||
baseHsvColor = new HsvColor(1.0, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); |
|||
} |
|||
|
|||
// Convert HSV to RGB once
|
|||
if (colorModel == ColorModel.Rgba) |
|||
{ |
|||
baseRgbColor = baseHsvColor.ToRgb(); |
|||
} |
|||
|
|||
// Maximize Saturation and Value components when in HSVA mode
|
|||
if (isSaturationValueMaxForced && |
|||
colorModel == ColorModel.Hsva && |
|||
component != ColorComponent.Alpha) |
|||
{ |
|||
switch (component) |
|||
{ |
|||
case ColorComponent.Component1: |
|||
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, 1.0); |
|||
break; |
|||
case ColorComponent.Component2: |
|||
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, 1.0); |
|||
break; |
|||
case ColorComponent.Component3: |
|||
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, baseHsvColor.V); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
// Create the color component gradient
|
|||
if (orientation == Orientation.Horizontal) |
|||
{ |
|||
// Determine the numerical increment of the color steps within the component
|
|||
if (colorModel == ColorModel.Hsva) |
|||
{ |
|||
if (component == ColorComponent.Component1) |
|||
{ |
|||
componentStep = 360.0 / width; |
|||
} |
|||
else |
|||
{ |
|||
componentStep = 1.0 / width; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
componentStep = 255.0 / width; |
|||
} |
|||
|
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
if (y == 0) |
|||
{ |
|||
rgbColor = GetColor(x * componentStep); |
|||
|
|||
// Get a new color
|
|||
bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255); |
|||
bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255); |
|||
bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255); |
|||
bgraPixelData[pixelDataIndex + 3] = rgbColor.A; |
|||
} |
|||
else |
|||
{ |
|||
// Use the color in the row above
|
|||
// Remember the pixel data is 1 dimensional instead of 2
|
|||
bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex + 0 - bgraPixelDataWidth]; |
|||
bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex + 1 - bgraPixelDataWidth]; |
|||
bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex + 2 - bgraPixelDataWidth]; |
|||
bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex + 3 - bgraPixelDataWidth]; |
|||
} |
|||
|
|||
pixelDataIndex += 4; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Determine the numerical increment of the color steps within the component
|
|||
if (colorModel == ColorModel.Hsva) |
|||
{ |
|||
if (component == ColorComponent.Component1) |
|||
{ |
|||
componentStep = 360.0 / height; |
|||
} |
|||
else |
|||
{ |
|||
componentStep = 1.0 / height; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
componentStep = 255.0 / height; |
|||
} |
|||
|
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
if (x == 0) |
|||
{ |
|||
// The lowest component value should be at the 'bottom' of the bitmap
|
|||
rgbColor = GetColor((height - 1 - y) * componentStep); |
|||
|
|||
// Get a new color
|
|||
bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255); |
|||
bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255); |
|||
bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255); |
|||
bgraPixelData[pixelDataIndex + 3] = rgbColor.A; |
|||
} |
|||
else |
|||
{ |
|||
// Use the color in the column to the left
|
|||
// Remember the pixel data is 1 dimensional instead of 2
|
|||
bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex - 4]; |
|||
bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex - 3]; |
|||
bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex - 2]; |
|||
bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex - 1]; |
|||
} |
|||
|
|||
pixelDataIndex += 4; |
|||
} |
|||
} |
|||
} |
|||
|
|||
Color GetColor(double componentValue) |
|||
{ |
|||
Color newRgbColor = Colors.White; |
|||
|
|||
switch (component) |
|||
{ |
|||
case ColorComponent.Component1: |
|||
{ |
|||
if (colorModel == ColorModel.Hsva) |
|||
{ |
|||
// Sweep hue
|
|||
newRgbColor = HsvColor.ToRgb( |
|||
MathUtilities.Clamp(componentValue, 0.0, 360.0), |
|||
baseHsvColor.S, |
|||
baseHsvColor.V, |
|||
baseHsvColor.A); |
|||
} |
|||
else |
|||
{ |
|||
// Sweep red
|
|||
newRgbColor = new Color( |
|||
baseRgbColor.A, |
|||
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), |
|||
baseRgbColor.G, |
|||
baseRgbColor.B); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
case ColorComponent.Component2: |
|||
{ |
|||
if (colorModel == ColorModel.Hsva) |
|||
{ |
|||
// Sweep saturation
|
|||
newRgbColor = HsvColor.ToRgb( |
|||
baseHsvColor.H, |
|||
MathUtilities.Clamp(componentValue, 0.0, 1.0), |
|||
baseHsvColor.V, |
|||
baseHsvColor.A); |
|||
} |
|||
else |
|||
{ |
|||
// Sweep green
|
|||
newRgbColor = new Color( |
|||
baseRgbColor.A, |
|||
baseRgbColor.R, |
|||
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), |
|||
baseRgbColor.B); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
case ColorComponent.Component3: |
|||
{ |
|||
if (colorModel == ColorModel.Hsva) |
|||
{ |
|||
// Sweep value
|
|||
newRgbColor = HsvColor.ToRgb( |
|||
baseHsvColor.H, |
|||
baseHsvColor.S, |
|||
MathUtilities.Clamp(componentValue, 0.0, 1.0), |
|||
baseHsvColor.A); |
|||
} |
|||
else |
|||
{ |
|||
// Sweep blue
|
|||
newRgbColor = new Color( |
|||
baseRgbColor.A, |
|||
baseRgbColor.R, |
|||
baseRgbColor.G, |
|||
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0))); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
case ColorComponent.Alpha: |
|||
{ |
|||
if (colorModel == ColorModel.Hsva) |
|||
{ |
|||
// Sweep alpha
|
|||
newRgbColor = HsvColor.ToRgb( |
|||
baseHsvColor.H, |
|||
baseHsvColor.S, |
|||
baseHsvColor.V, |
|||
MathUtilities.Clamp(componentValue, 0.0, 1.0)); |
|||
} |
|||
else |
|||
{ |
|||
// Sweep alpha
|
|||
newRgbColor = new Color( |
|||
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), |
|||
baseRgbColor.R, |
|||
baseRgbColor.G, |
|||
baseRgbColor.B); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
return newRgbColor; |
|||
} |
|||
|
|||
return bgraPixelData; |
|||
}); |
|||
|
|||
return bitmap; |
|||
} |
|||
|
|||
public static Hsv IncrementColorComponent( |
|||
Hsv originalHsv, |
|||
HsvComponent component, |
|||
IncrementDirection direction, |
|||
IncrementAmount amount, |
|||
bool shouldWrap, |
|||
double minBound, |
|||
double maxBound) |
|||
{ |
|||
Hsv newHsv = originalHsv; |
|||
|
|||
if (amount == IncrementAmount.Small || !ColorHelper.ToDisplayNameExists) |
|||
{ |
|||
// In order to avoid working with small values that can incur rounding issues,
|
|||
// we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1.
|
|||
newHsv.S *= 100; |
|||
newHsv.V *= 100; |
|||
|
|||
// Note: *valueToIncrement replaced with ref local variable for C#, must be initialized
|
|||
ref double valueToIncrement = ref newHsv.H; |
|||
double incrementAmount = 0.0; |
|||
|
|||
// If we're adding a small increment, then we'll just add or subtract 1.
|
|||
// If we're adding a large increment, then we want to snap to the next
|
|||
// or previous major value - for hue, this is every increment of 30;
|
|||
// for saturation and value, this is every increment of 10.
|
|||
switch (component) |
|||
{ |
|||
case HsvComponent.Hue: |
|||
valueToIncrement = ref newHsv.H; |
|||
incrementAmount = amount == IncrementAmount.Small ? 1 : 30; |
|||
break; |
|||
|
|||
case HsvComponent.Saturation: |
|||
valueToIncrement = ref newHsv.S; |
|||
incrementAmount = amount == IncrementAmount.Small ? 1 : 10; |
|||
break; |
|||
|
|||
case HsvComponent.Value: |
|||
valueToIncrement = ref newHsv.V; |
|||
incrementAmount = amount == IncrementAmount.Small ? 1 : 10; |
|||
break; |
|||
|
|||
default: |
|||
throw new InvalidOperationException("Invalid HsvComponent."); |
|||
} |
|||
|
|||
double previousValue = valueToIncrement; |
|||
|
|||
valueToIncrement += (direction == IncrementDirection.Lower ? -incrementAmount : incrementAmount); |
|||
|
|||
// If the value has reached outside the bounds, we were previous at the boundary, and we should wrap,
|
|||
// then we'll place the selection on the other side of the spectrum.
|
|||
// Otherwise, we'll place it on the boundary that was exceeded.
|
|||
if (valueToIncrement < minBound) |
|||
{ |
|||
valueToIncrement = (shouldWrap && previousValue == minBound) ? maxBound : minBound; |
|||
} |
|||
|
|||
if (valueToIncrement > maxBound) |
|||
{ |
|||
valueToIncrement = (shouldWrap && previousValue == maxBound) ? minBound : maxBound; |
|||
} |
|||
|
|||
// We multiplied saturation and value by 100 previously, so now we want to put them back in the 0-1 range.
|
|||
newHsv.S /= 100; |
|||
newHsv.V /= 100; |
|||
} |
|||
else |
|||
{ |
|||
// While working with named colors, we're going to need to be working in actual HSV units,
|
|||
// so we'll divide the min bound and max bound by 100 in the case of saturation or value,
|
|||
// since we'll have received units between 0-100 and we need them within 0-1.
|
|||
if (component == HsvComponent.Saturation || |
|||
component == HsvComponent.Value) |
|||
{ |
|||
minBound /= 100; |
|||
maxBound /= 100; |
|||
} |
|||
|
|||
newHsv = FindNextNamedColor(originalHsv, component, direction, shouldWrap, minBound, maxBound); |
|||
} |
|||
|
|||
return newHsv; |
|||
} |
|||
|
|||
public static Hsv FindNextNamedColor( |
|||
Hsv originalHsv, |
|||
HsvComponent component, |
|||
IncrementDirection direction, |
|||
bool shouldWrap, |
|||
double minBound, |
|||
double maxBound) |
|||
{ |
|||
// There's no easy way to directly get the next named color, so what we'll do
|
|||
// is just iterate in the direction that we want to find it until we find a color
|
|||
// in that direction that has a color name different than our current color name.
|
|||
// Once we find a new color name, then we'll iterate across that color name until
|
|||
// we find its bounds on the other side, and then select the color that is exactly
|
|||
// in the middle of that color's bounds.
|
|||
Hsv newHsv = originalHsv; |
|||
|
|||
string originalColorName = ColorHelper.ToDisplayName(originalHsv.ToRgb().ToColor()); |
|||
string newColorName = originalColorName; |
|||
|
|||
// Note: *newValue replaced with ref local variable for C#, must be initialized
|
|||
double originalValue = 0.0; |
|||
ref double newValue = ref newHsv.H; |
|||
double incrementAmount = 0.0; |
|||
|
|||
switch (component) |
|||
{ |
|||
case HsvComponent.Hue: |
|||
originalValue = originalHsv.H; |
|||
newValue = ref newHsv.H; |
|||
incrementAmount = 1; |
|||
break; |
|||
|
|||
case HsvComponent.Saturation: |
|||
originalValue = originalHsv.S; |
|||
newValue = ref newHsv.S; |
|||
incrementAmount = 0.01; |
|||
break; |
|||
|
|||
case HsvComponent.Value: |
|||
originalValue = originalHsv.V; |
|||
newValue = ref newHsv.V; |
|||
incrementAmount = 0.01; |
|||
break; |
|||
|
|||
default: |
|||
throw new InvalidOperationException("Invalid HsvComponent."); |
|||
} |
|||
|
|||
bool shouldFindMidPoint = true; |
|||
|
|||
while (newColorName == originalColorName) |
|||
{ |
|||
double previousValue = newValue; |
|||
newValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; |
|||
|
|||
bool justWrapped = false; |
|||
|
|||
// If we've hit a boundary, then either we should wrap or we shouldn't.
|
|||
// If we should, then we'll perform that wrapping if we were previously up against
|
|||
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
|
|||
if (newValue > maxBound) |
|||
{ |
|||
if (shouldWrap) |
|||
{ |
|||
newValue = minBound; |
|||
justWrapped = true; |
|||
} |
|||
else |
|||
{ |
|||
newValue = maxBound; |
|||
shouldFindMidPoint = false; |
|||
newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor()); |
|||
break; |
|||
} |
|||
} |
|||
else if (newValue < minBound) |
|||
{ |
|||
if (shouldWrap) |
|||
{ |
|||
newValue = maxBound; |
|||
justWrapped = true; |
|||
} |
|||
else |
|||
{ |
|||
newValue = minBound; |
|||
shouldFindMidPoint = false; |
|||
newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor()); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (!justWrapped && |
|||
previousValue != originalValue && |
|||
Math.Sign(newValue - originalValue) != Math.Sign(previousValue - originalValue)) |
|||
{ |
|||
// If we've wrapped all the way back to the start and have failed to find a new color name,
|
|||
// then we'll just quit - there isn't a new color name that we're going to find.
|
|||
shouldFindMidPoint = false; |
|||
break; |
|||
} |
|||
|
|||
newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor()); |
|||
} |
|||
|
|||
if (shouldFindMidPoint) |
|||
{ |
|||
Hsv startHsv = newHsv; |
|||
Hsv currentHsv = startHsv; |
|||
double startEndOffset = 0; |
|||
string currentColorName = newColorName; |
|||
|
|||
// Note: *startValue/*currentValue replaced with ref local variables for C#, must be initialized
|
|||
ref double startValue = ref startHsv.H; |
|||
ref double currentValue = ref currentHsv.H; |
|||
double wrapIncrement = 0; |
|||
|
|||
switch (component) |
|||
{ |
|||
case HsvComponent.Hue: |
|||
startValue = ref startHsv.H; |
|||
currentValue = ref currentHsv.H; |
|||
wrapIncrement = 360.0; |
|||
break; |
|||
|
|||
case HsvComponent.Saturation: |
|||
startValue = ref startHsv.S; |
|||
currentValue = ref currentHsv.S; |
|||
wrapIncrement = 1.0; |
|||
break; |
|||
|
|||
case HsvComponent.Value: |
|||
startValue = ref startHsv.V; |
|||
currentValue = ref currentHsv.V; |
|||
wrapIncrement = 1.0; |
|||
break; |
|||
|
|||
default: |
|||
throw new InvalidOperationException("Invalid HsvComponent."); |
|||
} |
|||
|
|||
while (newColorName == currentColorName) |
|||
{ |
|||
currentValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; |
|||
|
|||
// If we've hit a boundary, then either we should wrap or we shouldn't.
|
|||
// If we should, then we'll perform that wrapping if we were previously up against
|
|||
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
|
|||
if (currentValue > maxBound) |
|||
{ |
|||
if (shouldWrap) |
|||
{ |
|||
currentValue = minBound; |
|||
startEndOffset = maxBound - minBound; |
|||
} |
|||
else |
|||
{ |
|||
currentValue = maxBound; |
|||
break; |
|||
} |
|||
} |
|||
else if (currentValue < minBound) |
|||
{ |
|||
if (shouldWrap) |
|||
{ |
|||
currentValue = maxBound; |
|||
startEndOffset = minBound - maxBound; |
|||
} |
|||
else |
|||
{ |
|||
currentValue = minBound; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
currentColorName = ColorHelper.ToDisplayName(currentHsv.ToRgb().ToColor()); |
|||
} |
|||
|
|||
newValue = (startValue + currentValue + startEndOffset) / 2; |
|||
|
|||
// Dividing by 2 may have gotten us halfway through a single step, so we'll
|
|||
// remove that half-step if it exists.
|
|||
double leftoverValue = Math.Abs(newValue); |
|||
|
|||
while (leftoverValue > incrementAmount) |
|||
{ |
|||
leftoverValue -= incrementAmount; |
|||
} |
|||
|
|||
newValue -= leftoverValue; |
|||
|
|||
while (newValue < minBound) |
|||
{ |
|||
newValue += wrapIncrement; |
|||
} |
|||
|
|||
while (newValue > maxBound) |
|||
{ |
|||
newValue -= wrapIncrement; |
|||
} |
|||
} |
|||
|
|||
return newHsv; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts the given raw BGRA pre-multiplied alpha pixel data into a bitmap.
|
|||
/// </summary>
|
|||
/// <param name="bgraPixelData">The bitmap (in raw BGRA pre-multiplied alpha pixels).</param>
|
|||
/// <param name="pixelWidth">The pixel width of the bitmap.</param>
|
|||
/// <param name="pixelHeight">The pixel height of the bitmap.</param>
|
|||
/// <returns>A new <see cref="WriteableBitmap"/>.</returns>
|
|||
public static WriteableBitmap CreateBitmapFromPixelData( |
|||
IList<byte> bgraPixelData, |
|||
int pixelWidth, |
|||
int pixelHeight) |
|||
{ |
|||
// Standard may need to change on some devices
|
|||
Vector dpi = new Vector(96, 96); |
|||
|
|||
var bitmap = new WriteableBitmap( |
|||
new PixelSize(pixelWidth, pixelHeight), |
|||
dpi, |
|||
PixelFormat.Bgra8888, |
|||
AlphaFormat.Premul); |
|||
|
|||
// Warning: This is highly questionable
|
|||
using (var frameBuffer = bitmap.Lock()) |
|||
{ |
|||
Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); |
|||
} |
|||
|
|||
return bitmap; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Defines a specific component in the RGB color model.
|
|||
/// </summary>
|
|||
public enum RgbComponent |
|||
{ |
|||
/// <summary>
|
|||
/// The Alpha component.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Also see: <see cref="Color.A"/>
|
|||
/// </remarks>
|
|||
Alpha = 0, |
|||
|
|||
/// <summary>
|
|||
/// The Red component.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Also see: <see cref="Color.R"/>
|
|||
/// </remarks>
|
|||
Red = 1, |
|||
|
|||
/// <summary>
|
|||
/// The Green component.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Also see: <see cref="Color.G"/>
|
|||
/// </remarks>
|
|||
Green = 2, |
|||
|
|||
/// <summary>
|
|||
/// The Blue component.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Also see: <see cref="Color.B"/>
|
|||
/// </remarks>
|
|||
Blue = 3 |
|||
}; |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:converters="using:Avalonia.Controls.Converters" |
|||
xmlns:pc="using:Avalonia.Controls.Primitives.Converters" |
|||
x:CompileBindings="True"> |
|||
|
|||
<Styles.Resources> |
|||
<pc:AccentColorConverter x:Key="AccentColor" /> |
|||
<converters:ToBrushConverter x:Key="ToBrush" /> |
|||
<converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/> |
|||
<converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/> |
|||
</Styles.Resources> |
|||
|
|||
<Style Selector="ColorPreviewer"> |
|||
<Setter Property="Height" Value="70" /> |
|||
<Setter Property="CornerRadius" Value="0" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid ColumnDefinitions="Auto,*,Auto"> |
|||
<!-- Left accent colors --> |
|||
<Grid Grid.Column="0" |
|||
Height="40" |
|||
Width="80" |
|||
ColumnDefinitions="*,*" |
|||
Margin="0,0,-10,0" |
|||
VerticalAlignment="Center" |
|||
IsVisible="{TemplateBinding ShowAccentColors}"> |
|||
<Border Grid.Column="0" |
|||
Grid.ColumnSpan="2" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Background="{StaticResource CheckeredBackgroundBrush}" /> |
|||
<Border x:Name="AccentDec2Border" |
|||
Grid.Column="0" |
|||
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}" |
|||
Tag="-2" |
|||
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" /> |
|||
<Border x:Name="AccentDec1Border" |
|||
Grid.Column="1" |
|||
Tag="-1" |
|||
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-1'}" /> |
|||
</Grid> |
|||
<!-- Right accent colors --> |
|||
<Grid Grid.Column="2" |
|||
Height="40" |
|||
Width="80" |
|||
ColumnDefinitions="*,*" |
|||
Margin="-10,0,0,0" |
|||
VerticalAlignment="Center" |
|||
IsVisible="{TemplateBinding ShowAccentColors}"> |
|||
<Border Grid.Column="0" |
|||
Grid.ColumnSpan="2" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Background="{StaticResource CheckeredBackgroundBrush}" /> |
|||
<Border x:Name="AccentInc1Border" |
|||
Grid.Column="0" |
|||
Tag="1" |
|||
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='1'}" /> |
|||
<Border x:Name="AccentInc2Border" |
|||
Grid.Column="1" |
|||
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource RightCornerRadiusFilterConverter}}" |
|||
Tag="2" |
|||
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='2'}" /> |
|||
</Grid> |
|||
<!-- Must be last for drop shadow Z-index --> |
|||
<Border Grid.Column="1" |
|||
BoxShadow="0 0 10 2 #BF000000" |
|||
CornerRadius="{TemplateBinding CornerRadius}" |
|||
Margin="10"> |
|||
<Panel> |
|||
<Border Background="{StaticResource CheckeredBackgroundBrush}" |
|||
CornerRadius="{TemplateBinding CornerRadius}" /> |
|||
<Border x:Name="PreviewBorder" |
|||
CornerRadius="{TemplateBinding CornerRadius}" |
|||
Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrush}}" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" /> |
|||
</Panel> |
|||
</Border> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
</Styles> |
|||
@ -0,0 +1,194 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:converters="using:Avalonia.Controls.Converters" |
|||
x:CompileBindings="True"> |
|||
|
|||
<Styles.Resources> |
|||
<converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" /> |
|||
<converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" /> |
|||
</Styles.Resources> |
|||
|
|||
<Style Selector="Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="BorderThickness" Value="0" /> |
|||
<Setter Property="Template"> |
|||
<Setter.Value> |
|||
<ControlTemplate> |
|||
<Border Background="{TemplateBinding Background}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
BorderThickness="{TemplateBinding BorderThickness}" |
|||
CornerRadius="10" /> |
|||
</ControlTemplate> |
|||
</Setter.Value> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="ColorSlider:horizontal"> |
|||
<Setter Property="BorderThickness" Value="0" /> |
|||
<Setter Property="CornerRadius" Value="10" /> |
|||
<Setter Property="Height" Value="20" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border BorderThickness="{TemplateBinding BorderThickness}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
CornerRadius="{TemplateBinding CornerRadius}"> |
|||
<Grid Margin="{TemplateBinding Padding}"> |
|||
<Rectangle HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="{StaticResource CheckeredBackgroundBrush}" |
|||
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" |
|||
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> |
|||
<Rectangle HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="{TemplateBinding Background}" |
|||
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" |
|||
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> |
|||
<Track Name="PART_Track" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Minimum="{TemplateBinding Minimum}" |
|||
Maximum="{TemplateBinding Maximum}" |
|||
Value="{TemplateBinding Value, Mode=TwoWay}" |
|||
IsDirectionReversed="{TemplateBinding IsDirectionReversed}" |
|||
Orientation="Horizontal"> |
|||
<Track.DecreaseButton> |
|||
<RepeatButton Name="PART_DecreaseButton" |
|||
Background="Transparent" |
|||
Focusable="False" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<RepeatButton.Template> |
|||
<ControlTemplate> |
|||
<Border Name="FocusTarget" |
|||
Background="Transparent" |
|||
Margin="0,-10" /> |
|||
</ControlTemplate> |
|||
</RepeatButton.Template> |
|||
</RepeatButton> |
|||
</Track.DecreaseButton> |
|||
<Track.IncreaseButton> |
|||
<RepeatButton Name="PART_IncreaseButton" |
|||
Background="Transparent" |
|||
Focusable="False" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<RepeatButton.Template> |
|||
<ControlTemplate> |
|||
<Border Name="FocusTarget" |
|||
Background="Transparent" |
|||
Margin="0,-10" /> |
|||
</ControlTemplate> |
|||
</RepeatButton.Template> |
|||
</RepeatButton> |
|||
</Track.IncreaseButton> |
|||
<Thumb Classes="ColorSliderThumbStyle" |
|||
Name="ColorSliderThumb" |
|||
Margin="0" |
|||
Padding="0" |
|||
DataContext="{TemplateBinding Value}" |
|||
Height="{TemplateBinding Height}" |
|||
Width="{TemplateBinding Height}" /> |
|||
</Track> |
|||
</Grid> |
|||
</Border> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="ColorSlider:vertical"> |
|||
<Setter Property="BorderThickness" Value="0" /> |
|||
<Setter Property="CornerRadius" Value="10" /> |
|||
<Setter Property="Width" Value="20" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border BorderThickness="{TemplateBinding BorderThickness}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
CornerRadius="{TemplateBinding CornerRadius}"> |
|||
<Grid Margin="{TemplateBinding Padding}"> |
|||
<Rectangle HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="{StaticResource CheckeredBackgroundBrush}" |
|||
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" |
|||
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> |
|||
<Rectangle HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="{TemplateBinding Background}" |
|||
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" |
|||
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> |
|||
<Track Name="PART_Track" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Minimum="{TemplateBinding Minimum}" |
|||
Maximum="{TemplateBinding Maximum}" |
|||
Value="{TemplateBinding Value, Mode=TwoWay}" |
|||
IsDirectionReversed="{TemplateBinding IsDirectionReversed}" |
|||
Orientation="Vertical"> |
|||
<Track.DecreaseButton> |
|||
<RepeatButton Name="PART_DecreaseButton" |
|||
Background="Transparent" |
|||
Focusable="False" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<RepeatButton.Template> |
|||
<ControlTemplate> |
|||
<Border Name="FocusTarget" |
|||
Background="Transparent" |
|||
Margin="0,-10" /> |
|||
</ControlTemplate> |
|||
</RepeatButton.Template> |
|||
</RepeatButton> |
|||
</Track.DecreaseButton> |
|||
<Track.IncreaseButton> |
|||
<RepeatButton Name="PART_IncreaseButton" |
|||
Background="Transparent" |
|||
Focusable="False" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<RepeatButton.Template> |
|||
<ControlTemplate> |
|||
<Border Name="FocusTarget" |
|||
Background="Transparent" |
|||
Margin="0,-10" /> |
|||
</ControlTemplate> |
|||
</RepeatButton.Template> |
|||
</RepeatButton> |
|||
</Track.IncreaseButton> |
|||
<Thumb Classes="ColorSliderThumbStyle" |
|||
Name="ColorSliderThumb" |
|||
Margin="0" |
|||
Padding="0" |
|||
DataContext="{TemplateBinding Value}" |
|||
Height="{TemplateBinding Width}" |
|||
Width="{TemplateBinding Width}" /> |
|||
</Track> |
|||
</Grid> |
|||
</Border> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<!-- Normal State --> |
|||
<Style Selector="ColorSlider /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="Background" Value="Transparent" /> |
|||
<Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" /> |
|||
<Setter Property="BorderThickness" Value="3" /> |
|||
</Style> |
|||
|
|||
<!-- Selector/Thumb Color --> |
|||
<Style Selector="ColorSlider:pointerover /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="Opacity" Value="0.75" /> |
|||
</Style> |
|||
<Style Selector="ColorSlider:pointerover:dark-selector /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="Opacity" Value="0.7" /> |
|||
</Style> |
|||
<Style Selector="ColorSlider:pointerover:light-selector /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="Opacity" Value="0.8" /> |
|||
</Style> |
|||
|
|||
<Style Selector="ColorSlider:dark-selector /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="BorderBrush" Value="Black" /> |
|||
</Style> |
|||
<Style Selector="ColorSlider:light-selector /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="BorderBrush" Value="White" /> |
|||
</Style> |
|||
|
|||
</Styles> |
|||
@ -0,0 +1,28 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
|
|||
<Styles.Resources> |
|||
<VisualBrush x:Key="CheckeredBackgroundBrush" |
|||
TileMode="Tile" |
|||
Stretch="Uniform" |
|||
DestinationRect="0,0,8,8"> |
|||
<VisualBrush.Visual> |
|||
<DrawingPresenter Width="8" |
|||
Height="8"> |
|||
<DrawingGroup> |
|||
<GeometryDrawing Geometry="M0,0 L2,0 2,2, 0,2Z" |
|||
Brush="Transparent" /> |
|||
<GeometryDrawing Geometry="M0,1 L2,1 2,2, 1,2 1,0 0,0Z" |
|||
Brush="#19808080" /> |
|||
</DrawingGroup> |
|||
</DrawingPresenter> |
|||
</VisualBrush.Visual> |
|||
</VisualBrush> |
|||
</Styles.Resources> |
|||
|
|||
<!-- Primitives --> |
|||
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml" /> |
|||
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml" /> |
|||
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml" /> |
|||
|
|||
</Styles> |
|||
@ -0,0 +1,86 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:converters="using:Avalonia.Controls.Converters" |
|||
xmlns:pc="using:Avalonia.Controls.Primitives.Converters" |
|||
x:CompileBindings="True"> |
|||
|
|||
<Styles.Resources> |
|||
<pc:AccentColorConverter x:Key="AccentColor" /> |
|||
<converters:ToBrushConverter x:Key="ToBrush" /> |
|||
<converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/> |
|||
<converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/> |
|||
</Styles.Resources> |
|||
|
|||
<Style Selector="ColorPreviewer"> |
|||
<Setter Property="Height" Value="70" /> |
|||
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid ColumnDefinitions="Auto,*,Auto"> |
|||
<!-- Left accent colors --> |
|||
<Grid Grid.Column="0" |
|||
Height="40" |
|||
Width="80" |
|||
ColumnDefinitions="*,*" |
|||
Margin="0,0,-10,0" |
|||
VerticalAlignment="Center" |
|||
IsVisible="{TemplateBinding ShowAccentColors}"> |
|||
<Border Grid.Column="0" |
|||
Grid.ColumnSpan="2" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Background="{StaticResource CheckeredBackgroundBrush}" /> |
|||
<Border x:Name="AccentDec2Border" |
|||
Grid.Column="0" |
|||
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}" |
|||
Tag="-2" |
|||
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" /> |
|||
<Border x:Name="AccentDec1Border" |
|||
Grid.Column="1" |
|||
Tag="-1" |
|||
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-1'}" /> |
|||
</Grid> |
|||
<!-- Right accent colors --> |
|||
<Grid Grid.Column="2" |
|||
Height="40" |
|||
Width="80" |
|||
ColumnDefinitions="*,*" |
|||
Margin="-10,0,0,0" |
|||
VerticalAlignment="Center" |
|||
IsVisible="{TemplateBinding ShowAccentColors}"> |
|||
<Border Grid.Column="0" |
|||
Grid.ColumnSpan="2" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Background="{StaticResource CheckeredBackgroundBrush}" /> |
|||
<Border x:Name="AccentInc1Border" |
|||
Grid.Column="0" |
|||
Tag="1" |
|||
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='1'}" /> |
|||
<Border x:Name="AccentInc2Border" |
|||
Grid.Column="1" |
|||
CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource RightCornerRadiusFilterConverter}}" |
|||
Tag="2" |
|||
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='2'}" /> |
|||
</Grid> |
|||
<!-- Must be last for drop shadow Z-index --> |
|||
<Border Grid.Column="1" |
|||
BoxShadow="0 0 10 2 #BF000000" |
|||
CornerRadius="{TemplateBinding CornerRadius}" |
|||
Margin="10"> |
|||
<Panel> |
|||
<Border Background="{StaticResource CheckeredBackgroundBrush}" |
|||
CornerRadius="{TemplateBinding CornerRadius}" /> |
|||
<Border x:Name="PreviewBorder" |
|||
CornerRadius="{TemplateBinding CornerRadius}" |
|||
Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrush}}" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" /> |
|||
</Panel> |
|||
</Border> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
</Styles> |
|||
@ -0,0 +1,194 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:converters="using:Avalonia.Controls.Converters" |
|||
x:CompileBindings="True"> |
|||
|
|||
<Styles.Resources> |
|||
<converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" /> |
|||
<converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" /> |
|||
</Styles.Resources> |
|||
|
|||
<Style Selector="Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="BorderThickness" Value="0" /> |
|||
<Setter Property="Template"> |
|||
<Setter.Value> |
|||
<ControlTemplate> |
|||
<Border Background="{TemplateBinding Background}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
BorderThickness="{TemplateBinding BorderThickness}" |
|||
CornerRadius="10" /> |
|||
</ControlTemplate> |
|||
</Setter.Value> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="ColorSlider:horizontal"> |
|||
<Setter Property="BorderThickness" Value="0" /> |
|||
<Setter Property="CornerRadius" Value="10" /> |
|||
<Setter Property="Height" Value="20" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border BorderThickness="{TemplateBinding BorderThickness}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
CornerRadius="{TemplateBinding CornerRadius}"> |
|||
<Grid Margin="{TemplateBinding Padding}"> |
|||
<Rectangle HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="{StaticResource CheckeredBackgroundBrush}" |
|||
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" |
|||
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> |
|||
<Rectangle HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="{TemplateBinding Background}" |
|||
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" |
|||
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> |
|||
<Track Name="PART_Track" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Minimum="{TemplateBinding Minimum}" |
|||
Maximum="{TemplateBinding Maximum}" |
|||
Value="{TemplateBinding Value, Mode=TwoWay}" |
|||
IsDirectionReversed="{TemplateBinding IsDirectionReversed}" |
|||
Orientation="Horizontal"> |
|||
<Track.DecreaseButton> |
|||
<RepeatButton Name="PART_DecreaseButton" |
|||
Background="Transparent" |
|||
Focusable="False" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<RepeatButton.Template> |
|||
<ControlTemplate> |
|||
<Border Name="FocusTarget" |
|||
Background="Transparent" |
|||
Margin="0,-10" /> |
|||
</ControlTemplate> |
|||
</RepeatButton.Template> |
|||
</RepeatButton> |
|||
</Track.DecreaseButton> |
|||
<Track.IncreaseButton> |
|||
<RepeatButton Name="PART_IncreaseButton" |
|||
Background="Transparent" |
|||
Focusable="False" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<RepeatButton.Template> |
|||
<ControlTemplate> |
|||
<Border Name="FocusTarget" |
|||
Background="Transparent" |
|||
Margin="0,-10" /> |
|||
</ControlTemplate> |
|||
</RepeatButton.Template> |
|||
</RepeatButton> |
|||
</Track.IncreaseButton> |
|||
<Thumb Classes="ColorSliderThumbStyle" |
|||
Name="ColorSliderThumb" |
|||
Margin="0" |
|||
Padding="0" |
|||
DataContext="{TemplateBinding Value}" |
|||
Height="{TemplateBinding Height}" |
|||
Width="{TemplateBinding Height}" /> |
|||
</Track> |
|||
</Grid> |
|||
</Border> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<Style Selector="ColorSlider:vertical"> |
|||
<Setter Property="BorderThickness" Value="0" /> |
|||
<Setter Property="CornerRadius" Value="10" /> |
|||
<Setter Property="Width" Value="20" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border BorderThickness="{TemplateBinding BorderThickness}" |
|||
BorderBrush="{TemplateBinding BorderBrush}" |
|||
CornerRadius="{TemplateBinding CornerRadius}"> |
|||
<Grid Margin="{TemplateBinding Padding}"> |
|||
<Rectangle HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="{StaticResource CheckeredBackgroundBrush}" |
|||
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" |
|||
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> |
|||
<Rectangle HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Fill="{TemplateBinding Background}" |
|||
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}" |
|||
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" /> |
|||
<Track Name="PART_Track" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Minimum="{TemplateBinding Minimum}" |
|||
Maximum="{TemplateBinding Maximum}" |
|||
Value="{TemplateBinding Value, Mode=TwoWay}" |
|||
IsDirectionReversed="{TemplateBinding IsDirectionReversed}" |
|||
Orientation="Vertical"> |
|||
<Track.DecreaseButton> |
|||
<RepeatButton Name="PART_DecreaseButton" |
|||
Background="Transparent" |
|||
Focusable="False" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<RepeatButton.Template> |
|||
<ControlTemplate> |
|||
<Border Name="FocusTarget" |
|||
Background="Transparent" |
|||
Margin="0,-10" /> |
|||
</ControlTemplate> |
|||
</RepeatButton.Template> |
|||
</RepeatButton> |
|||
</Track.DecreaseButton> |
|||
<Track.IncreaseButton> |
|||
<RepeatButton Name="PART_IncreaseButton" |
|||
Background="Transparent" |
|||
Focusable="False" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch"> |
|||
<RepeatButton.Template> |
|||
<ControlTemplate> |
|||
<Border Name="FocusTarget" |
|||
Background="Transparent" |
|||
Margin="0,-10" /> |
|||
</ControlTemplate> |
|||
</RepeatButton.Template> |
|||
</RepeatButton> |
|||
</Track.IncreaseButton> |
|||
<Thumb Classes="ColorSliderThumbStyle" |
|||
Name="ColorSliderThumb" |
|||
Margin="0" |
|||
Padding="0" |
|||
DataContext="{TemplateBinding Value}" |
|||
Height="{TemplateBinding Width}" |
|||
Width="{TemplateBinding Width}" /> |
|||
</Track> |
|||
</Grid> |
|||
</Border> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<!-- Normal State --> |
|||
<Style Selector="ColorSlider /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="Background" Value="Transparent" /> |
|||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" /> |
|||
<Setter Property="BorderThickness" Value="3" /> |
|||
</Style> |
|||
|
|||
<!-- Selector/Thumb Color --> |
|||
<Style Selector="ColorSlider:pointerover /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="Opacity" Value="0.75" /> |
|||
</Style> |
|||
<Style Selector="ColorSlider:pointerover:dark-selector /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="Opacity" Value="0.7" /> |
|||
</Style> |
|||
<Style Selector="ColorSlider:pointerover:light-selector /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="Opacity" Value="0.8" /> |
|||
</Style> |
|||
|
|||
<Style Selector="ColorSlider:dark-selector /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlBackgroundChromeBlackHighBrush}" /> |
|||
</Style> |
|||
<Style Selector="ColorSlider:light-selector /template/ Thumb.ColorSliderThumbStyle"> |
|||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlBackgroundChromeWhiteBrush}" /> |
|||
</Style> |
|||
|
|||
</Styles> |
|||
@ -0,0 +1,28 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
|
|||
<Styles.Resources> |
|||
<VisualBrush x:Key="CheckeredBackgroundBrush" |
|||
TileMode="Tile" |
|||
Stretch="Uniform" |
|||
DestinationRect="0,0,8,8"> |
|||
<VisualBrush.Visual> |
|||
<DrawingPresenter Width="8" |
|||
Height="8"> |
|||
<DrawingGroup> |
|||
<GeometryDrawing Geometry="M0,0 L2,0 2,2, 0,2Z" |
|||
Brush="Transparent" /> |
|||
<GeometryDrawing Geometry="M0,1 L2,1 2,2, 1,2 1,0 0,0Z" |
|||
Brush="#19808080" /> |
|||
</DrawingGroup> |
|||
</DrawingPresenter> |
|||
</VisualBrush.Visual> |
|||
</VisualBrush> |
|||
</Styles.Resources> |
|||
|
|||
<!-- Primitives --> |
|||
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml" /> |
|||
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml" /> |
|||
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml" /> |
|||
|
|||
</Styles> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue