diff --git a/native/Avalonia.Native/src/OSX/AutoFitContentView.h b/native/Avalonia.Native/src/OSX/AutoFitContentView.h new file mode 100644 index 0000000000..68c9fb8dc8 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AutoFitContentView.h @@ -0,0 +1,15 @@ +// +// Created by Dan Walmsley on 05/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#import "avalonia-native.h" + +@interface AutoFitContentView : NSView +-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; +-(void) ShowTitleBar: (bool) show; +-(void) SetTitleBarHeightHint: (double) height; + +-(void) ShowBlur: (bool) show; +@end \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/AutoFitContentView.mm b/native/Avalonia.Native/src/OSX/AutoFitContentView.mm new file mode 100644 index 0000000000..92d6f67a91 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AutoFitContentView.mm @@ -0,0 +1,104 @@ +// +// Created by Dan Walmsley on 05/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#include "AvnView.h" +#import "AutoFitContentView.h" + +@implementation AutoFitContentView +{ + NSVisualEffectView* _titleBarMaterial; + NSBox* _titleBarUnderline; + NSView* _content; + NSVisualEffectView* _blurBehind; + double _titleBarHeightHint; + bool _settingSize; +} + +-(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content +{ + _titleBarHeightHint = -1; + _content = content; + _settingSize = false; + + [self setAutoresizesSubviews:true]; + [self setWantsLayer:true]; + + _titleBarMaterial = [NSVisualEffectView new]; + [_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow]; + [_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar]; + [_titleBarMaterial setWantsLayer:true]; + _titleBarMaterial.hidden = true; + + _titleBarUnderline = [NSBox new]; + _titleBarUnderline.boxType = NSBoxSeparator; + _titleBarUnderline.fillColor = [NSColor underPageBackgroundColor]; + _titleBarUnderline.hidden = true; + + [self addSubview:_titleBarMaterial]; + [self addSubview:_titleBarUnderline]; + + _blurBehind = [NSVisualEffectView new]; + [_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [_blurBehind setMaterial:NSVisualEffectMaterialLight]; + [_blurBehind setWantsLayer:true]; + _blurBehind.hidden = true; + + [_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + + [self addSubview:_blurBehind]; + [self addSubview:_content]; + + [self setWantsLayer:true]; + return self; +} + +-(void) ShowBlur:(bool)show +{ + _blurBehind.hidden = !show; +} + +-(void) ShowTitleBar: (bool) show +{ + _titleBarMaterial.hidden = !show; + _titleBarUnderline.hidden = !show; +} + +-(void) SetTitleBarHeightHint: (double) height +{ + _titleBarHeightHint = height; + + [self setFrameSize:self.frame.size]; +} + +-(void)setFrameSize:(NSSize)newSize +{ + if(_settingSize) + { + return; + } + + _settingSize = true; + [super setFrameSize:newSize]; + + auto window = objc_cast([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 \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 7571d51c9f..88e4c0c682 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -7,6 +7,18 @@ objects = { /* Begin PBXBuildFile section */ + 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391E45702740FE9DD69695 /* ResizeScope.mm */; }; + 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 183919BF108EB72A029F7671 /* WindowImpl.mm */; }; + 183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */; }; + 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391BBB7782C296D424071F /* INSWindowHolder.h */; }; + 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */; }; + 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391CD090AA776E7E841AC9 /* WindowImpl.h */; }; + 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839166350F32661F3ABD70F /* AutoFitContentView.mm */; }; + 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */ = {isa = PBXBuildFile; fileRef = 1839171D898F9BFC1373631A /* ResizeScope.h */; }; + 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */ = {isa = PBXBuildFile; fileRef = 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */; }; + 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1839132D0E2454D911F1D1F9 /* AvnView.mm */; }; + 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */; }; + 18391ED5F611FF62C45F196D /* AvnView.h in Headers */ = {isa = PBXBuildFile; fileRef = 18391D1669284AD2EC9E866A /* AvnView.h */; }; 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; }; 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; }; @@ -35,6 +47,18 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1839132D0E2454D911F1D1F9 /* AvnView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnView.mm; sourceTree = ""; }; + 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IWindowStateChanged.h; sourceTree = ""; }; + 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowBaseImpl.h; sourceTree = ""; }; + 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoFitContentView.h; sourceTree = ""; }; + 1839166350F32661F3ABD70F /* AutoFitContentView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AutoFitContentView.mm; sourceTree = ""; }; + 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowBaseImpl.mm; sourceTree = ""; }; + 1839171D898F9BFC1373631A /* ResizeScope.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResizeScope.h; sourceTree = ""; }; + 183919BF108EB72A029F7671 /* WindowImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WindowImpl.mm; sourceTree = ""; }; + 18391BBB7782C296D424071F /* INSWindowHolder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = INSWindowHolder.h; sourceTree = ""; }; + 18391CD090AA776E7E841AC9 /* WindowImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowImpl.h; sourceTree = ""; }; + 18391D1669284AD2EC9E866A /* AvnView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvnView.h; sourceTree = ""; }; + 18391E45702740FE9DD69695 /* ResizeScope.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ResizeScope.mm; sourceTree = ""; }; 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; }; 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = ""; }; @@ -130,6 +154,18 @@ 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, AB7A61F02147C815003C5833 /* Products */, AB661C1C2148230E00291242 /* Frameworks */, + 18391676ECF0E983F4964357 /* WindowBaseImpl.mm */, + 183915BFF0E234CD3604A7CD /* WindowBaseImpl.h */, + 18391BBB7782C296D424071F /* INSWindowHolder.h */, + 183919BF108EB72A029F7671 /* WindowImpl.mm */, + 18391CD090AA776E7E841AC9 /* WindowImpl.h */, + 183913C6BFD6856BD42D19FD /* IWindowStateChanged.h */, + 18391E45702740FE9DD69695 /* ResizeScope.mm */, + 1839171D898F9BFC1373631A /* ResizeScope.h */, + 1839132D0E2454D911F1D1F9 /* AvnView.mm */, + 18391D1669284AD2EC9E866A /* AvnView.h */, + 1839166350F32661F3ABD70F /* AutoFitContentView.mm */, + 18391654EF0E7AB3D3AB4071 /* AutoFitContentView.h */, ); sourceTree = ""; }; @@ -150,6 +186,13 @@ files = ( 37155CE4233C00EB0034DCE9 /* menu.h in Headers */, BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */, + 183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */, + 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */, + 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */, + 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */, + 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */, + 18391ED5F611FF62C45F196D /* AvnView.h in Headers */, + 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -229,6 +272,11 @@ AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, AB661C202148286E00291242 /* window.mm in Sources */, + 1839179A55FC1421BEE83330 /* WindowBaseImpl.mm in Sources */, + 1839125F057B0A4EB1760058 /* WindowImpl.mm in Sources */, + 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */, + 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */, + 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme index 5d20a135b9..87a8312c38 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme @@ -56,10 +56,14 @@ + + + + +#import +#import +#include "window.h" +#import "comimpl.h" +#import "common.h" +#import "WindowImpl.h" +#import "KeyTransform.h" + +@class AvnAccessibilityElement; + +@interface AvnView : NSView +-(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 \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm new file mode 100644 index 0000000000..24cbc25502 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -0,0 +1,706 @@ +// +// Created by Dan Walmsley on 05/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#import "AvnView.h" +#include "automation.h" + +@implementation AvnView +{ + ComPtr _parent; + NSTrackingArea* _area; + bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; + AvnInputModifiers _modifierState; + NSEvent* _lastMouseDownEvent; + bool _lastKeyHandled; + AvnPixelSize _lastPixelSize; + NSObject* _renderTarget; + AvnPlatformResizeReason _resizeReason; + AvnAccessibilityElement* _accessibilityChild; +} + +- (void)onClosed +{ + @synchronized (self) + { + _parent = nullptr; + } +} + +- (NSEvent*) lastMouseDownEvent +{ + return _lastMouseDownEvent; +} + +- (void) updateRenderTarget +{ + [_renderTarget resize:_lastPixelSize withScale:static_cast([[self window] backingScaleFactor])]; + [self setNeedsDisplayInRect:[self frame]]; +} + +-(AvnView*) initWithParent: (WindowBaseImpl*) parent +{ + self = [super init]; + _renderTarget = parent->renderTarget; + [self setWantsLayer:YES]; + [self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize]; + + _parent = parent; + _area = nullptr; + _lastPixelSize.Height = 100; + _lastPixelSize.Width = 100; + [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; + + _modifierState = AvnInputModifiersNone; + return self; +} + +- (BOOL)isFlipped +{ + return YES; +} + +- (BOOL)wantsUpdateLayer +{ + return YES; +} + +- (void)setLayer:(CALayer *)layer +{ + [_renderTarget setNewLayer: layer]; + [super setLayer: layer]; +} + +- (BOOL)isOpaque +{ + return YES; +} + +- (BOOL)acceptsFirstResponder +{ + return true; +} + +- (BOOL)acceptsFirstMouse:(NSEvent *)event +{ + return true; +} + +- (BOOL)canBecomeKeyView +{ + return true; +} + +-(void)setFrameSize:(NSSize)newSize +{ + [super setFrameSize:newSize]; + + if(_area != nullptr) + { + [self removeTrackingArea:_area]; + _area = nullptr; + } + + if (_parent == nullptr) + { + return; + } + + NSRect rect = NSZeroRect; + rect.size = newSize; + + NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingEnabledDuringMouseDrag; + _area = [[NSTrackingArea alloc] initWithRect:rect options:options owner:self userInfo:nullptr]; + [self addTrackingArea:_area]; + + _parent->UpdateCursor(); + + auto fsize = [self convertSizeToBacking: [self frame].size]; + + if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height) + { + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + [self updateRenderTarget]; + + auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); + } +} + +- (void)updateLayer +{ + AvnInsidePotentialDeadlock deadlock; + if (_parent == nullptr) + { + return; + } + + _parent->BaseEvents->RunRenderPriorityJobs(); + + if (_parent == nullptr) + { + return; + } + + _parent->BaseEvents->Paint(); +} + +- (void)drawRect:(NSRect)dirtyRect +{ + return; +} + +-(void) setSwRenderedFrame: (AvnFramebuffer*) fb dispose: (IUnknown*) dispose +{ + @autoreleasepool { + [_renderTarget setSwFrame:fb]; + dispose->Release(); + } +} + +- (AvnPoint) translateLocalPoint:(AvnPoint)pt +{ + pt.Y = [self bounds].size.height - pt.Y; + return pt; +} + ++ (AvnPoint)toAvnPoint:(CGPoint)p +{ + AvnPoint result; + + result.X = p.x; + result.Y = p.y; + + return result; +} + +- (void) viewDidChangeBackingProperties +{ + auto fsize = [self convertSizeToBacking: [self frame].size]; + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + [self updateRenderTarget]; + + if(_parent != nullptr) + { + _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); + } + + [super viewDidChangeBackingProperties]; +} + +- (bool) ignoreUserInput:(bool)trigerInputWhenDisabled +{ + auto parentWindow = objc_cast([self window]); + + if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) + { + if(trigerInputWhenDisabled) + { + auto window = dynamic_cast(_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([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([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 *)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 )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(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 )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender]; +} + +- (NSDragOperation)draggingUpdated:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender]; +} + +- (void)draggingExited:(id )sender +{ + [self triggerAvnDragEvent: AvnDragEventType::Leave info:sender]; +} + +- (BOOL)prepareForDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone; +} + +- (BOOL)performDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone; +} + +- (void)concludeDragOperation:(nullable id )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 \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/INSWindowHolder.h b/native/Avalonia.Native/src/OSX/INSWindowHolder.h new file mode 100644 index 0000000000..adc0dd1990 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/INSWindowHolder.h @@ -0,0 +1,17 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H +#define AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H + +@class AvnView; + +struct INSWindowHolder +{ + virtual AvnWindow* _Nonnull GetNSWindow () = 0; + virtual AvnView* _Nonnull GetNSView () = 0; +}; + +#endif //AVALONIA_NATIVE_OSX_INSWINDOWHOLDER_H diff --git a/native/Avalonia.Native/src/OSX/IWindowStateChanged.h b/native/Avalonia.Native/src/OSX/IWindowStateChanged.h new file mode 100644 index 0000000000..f0905da3ac --- /dev/null +++ b/native/Avalonia.Native/src/OSX/IWindowStateChanged.h @@ -0,0 +1,18 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H +#define AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H + +struct IWindowStateChanged +{ + virtual void WindowStateChanged () = 0; + virtual void StartStateTransition () = 0; + virtual void EndStateTransition () = 0; + virtual SystemDecorations Decorations () = 0; + virtual AvnWindowState WindowState () = 0; +}; + +#endif //AVALONIA_NATIVE_OSX_IWINDOWSTATECHANGED_H diff --git a/native/Avalonia.Native/src/OSX/ResizeScope.h b/native/Avalonia.Native/src/OSX/ResizeScope.h new file mode 100644 index 0000000000..7509f93c01 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/ResizeScope.h @@ -0,0 +1,25 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_RESIZESCOPE_H +#define AVALONIA_NATIVE_OSX_RESIZESCOPE_H + +#include "window.h" +#include "avalonia-native.h" + +@class AvnView; + +class ResizeScope +{ +public: + ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason); + + ~ResizeScope(); +private: + AvnView* _Nonnull _view; + AvnPlatformResizeReason _restore; +}; + +#endif //AVALONIA_NATIVE_OSX_RESIZESCOPE_H diff --git a/native/Avalonia.Native/src/OSX/ResizeScope.mm b/native/Avalonia.Native/src/OSX/ResizeScope.mm new file mode 100644 index 0000000000..90e7f5cf15 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/ResizeScope.mm @@ -0,0 +1,18 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#include "ResizeScope.h" +#import "AvnView.h" + +ResizeScope::ResizeScope(AvnView *view, AvnPlatformResizeReason reason) { + _view = view; + _restore = [view getResizeReason]; + [view setResizeReason:reason]; +} + +ResizeScope::~ResizeScope() { + [_view setResizeReason:_restore]; +} diff --git a/native/Avalonia.Native/src/OSX/SystemDialogs.mm b/native/Avalonia.Native/src/OSX/SystemDialogs.mm index a47221056b..21ad9cfa7c 100644 --- a/native/Avalonia.Native/src/OSX/SystemDialogs.mm +++ b/native/Avalonia.Native/src/OSX/SystemDialogs.mm @@ -1,5 +1,6 @@ #include "common.h" #include "window.h" +#include "INSWindowHolder.h" class SystemDialogs : public ComSingleObject { diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h new file mode 100644 index 0000000000..d2690cee41 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -0,0 +1,121 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H +#define AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H + +#import "rendertarget.h" +#include "INSWindowHolder.h" + +@class AutoFitContentView; + +class WindowBaseImpl : public virtual ComObject, + public virtual IAvnWindowBase, + public INSWindowHolder { +private: + NSCursor *cursor; + +public: + FORWARD_IUNKNOWN() + +BEGIN_INTERFACE_MAP() + INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase) + END_INTERFACE_MAP() + + virtual ~WindowBaseImpl() { + View = NULL; + Window = NULL; + } + + AutoFitContentView *StandardContainer; + AvnView *View; + AvnWindow *Window; + ComPtr BaseEvents; + ComPtr _glContext; + NSObject *renderTarget; + AvnPoint lastPositionSet; + NSString *_lastTitle; + + bool _shown; + bool _inResize; + + WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl); + + virtual HRESULT ObtainNSWindowHandle(void **ret) override; + + virtual HRESULT ObtainNSWindowHandleRetained(void **ret) override; + + virtual HRESULT ObtainNSViewHandle(void **ret) override; + + virtual HRESULT ObtainNSViewHandleRetained(void **ret) override; + + virtual AvnWindow *GetNSWindow() override; + + virtual AvnView *GetNSView() override; + + virtual HRESULT Show(bool activate, bool isDialog) override; + + virtual bool ShouldTakeFocusOnShow(); + + virtual HRESULT Hide() override; + + virtual HRESULT Activate() override; + + virtual HRESULT SetTopMost(bool value) override; + + virtual HRESULT Close() override; + + virtual HRESULT GetClientSize(AvnSize *ret) override; + + virtual HRESULT GetFrameSize(AvnSize *ret) override; + + virtual HRESULT GetScaling(double *ret) override; + + virtual HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize) override; + + virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override; + + virtual HRESULT Invalidate(__attribute__((unused)) AvnRect rect) override; + + virtual HRESULT SetMainMenu(IAvnMenu *menu) override; + + virtual HRESULT BeginMoveDrag() override; + + virtual HRESULT BeginResizeDrag(__attribute__((unused)) AvnWindowEdge edge) override; + + virtual HRESULT GetPosition(AvnPoint *ret) override; + + virtual HRESULT SetPosition(AvnPoint point) override; + + virtual HRESULT PointToClient(AvnPoint point, AvnPoint *ret) override; + + virtual HRESULT PointToScreen(AvnPoint point, AvnPoint *ret) override; + + virtual HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer *fb, IUnknown *dispose) override; + + virtual HRESULT SetCursor(IAvnCursor *cursor) override; + + virtual void UpdateCursor(); + + virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget **ppv) override; + + virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost **retOut) override; + + virtual HRESULT SetBlurEnabled(bool enable) override; + + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, + IAvnClipboard *clipboard, IAvnDndResultCallback *cb, + void *sourceHandle) override; + + virtual bool IsDialog(); + +protected: + virtual NSWindowStyleMask GetStyle(); + + void UpdateStyle(); + +}; + +#endif //AVALONIA_NATIVE_OSX_WINDOWBASEIMPL_H diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm new file mode 100644 index 0000000000..fb67addce1 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -0,0 +1,505 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#include "common.h" +#import "window.h" +#import "AvnView.h" +#include "menu.h" +#include "automation.h" +#import "cursor.h" +#include "ResizeScope.h" +#import "AutoFitContentView.h" + +WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl) { + _shown = false; + _inResize = false; + BaseEvents = events; + _glContext = gl; + renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext:gl]; + View = [[AvnView alloc] initWithParent:this]; + StandardContainer = [[AutoFitContentView new] initWithContent:View]; + + Window = [[AvnWindow alloc] initWithParent:this]; + [Window setContentView:StandardContainer]; + + lastPositionSet.X = 100; + lastPositionSet.Y = 100; + _lastTitle = @""; + + [Window setStyleMask:NSWindowStyleMaskBorderless]; + [Window setBackingType:NSBackingStoreBuffered]; + + [Window setOpaque:false]; +} + +HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge void *) View; + + return S_OK; +} + +HRESULT WindowBaseImpl::ObtainNSViewHandleRetained(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge_retained void *) View; + + return S_OK; +} + +AvnWindow *WindowBaseImpl::GetNSWindow() { + return Window; +} + +AvnView *WindowBaseImpl::GetNSView() { + return View; +} + +HRESULT WindowBaseImpl::ObtainNSWindowHandleRetained(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge_retained void *) Window; + + return S_OK; +} + +HRESULT WindowBaseImpl::Show(bool activate, bool isDialog) { + START_COM_CALL; + + @autoreleasepool { + SetPosition(lastPositionSet); + UpdateStyle(); + + [Window setTitle:_lastTitle]; + + if (ShouldTakeFocusOnShow() && activate) { + [Window orderFront:Window]; + [Window makeKeyAndOrderFront:Window]; + [Window makeFirstResponder:View]; + [NSApp activateIgnoringOtherApps:YES]; + } else { + [Window orderFront:Window]; + } + + _shown = true; + + return S_OK; + } +} + +bool WindowBaseImpl::ShouldTakeFocusOnShow() { + return true; +} + +HRESULT WindowBaseImpl::ObtainNSWindowHandle(void **ret) { + START_COM_CALL; + + if (ret == nullptr) { + return E_POINTER; + } + + *ret = (__bridge void *) Window; + + return S_OK; +} + +HRESULT WindowBaseImpl::Hide() { + START_COM_CALL; + + @autoreleasepool { + if (Window != nullptr) { + [Window orderOut:Window]; + [Window restoreParentWindow]; + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Activate() { + START_COM_CALL; + + @autoreleasepool { + if (Window != nullptr) { + [Window makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + } + } + + return S_OK; +} + +HRESULT WindowBaseImpl::SetTopMost(bool value) { + START_COM_CALL; + + @autoreleasepool { + [Window setLevel:value ? NSFloatingWindowLevel : NSNormalWindowLevel]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Close() { + START_COM_CALL; + + @autoreleasepool { + if (Window != nullptr) { + auto window = Window; + Window = nullptr; + + try { + // Seems to throw sometimes on application exit. + [window close]; + } + catch (NSException *) {} + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::GetClientSize(AvnSize *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) + return E_POINTER; + + auto frame = [View frame]; + ret->Width = frame.size.width; + ret->Height = frame.size.height; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::GetFrameSize(AvnSize *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) + return E_POINTER; + + auto frame = [Window frame]; + ret->Width = frame.size.width; + ret->Height = frame.size.height; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::GetScaling(double *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) + return E_POINTER; + + if (Window == nullptr) { + *ret = 1; + return S_OK; + } + + *ret = [Window backingScaleFactor]; + return S_OK; + } +} + +HRESULT WindowBaseImpl::SetMinMaxSize(AvnSize minSize, AvnSize maxSize) { + START_COM_CALL; + + @autoreleasepool { + [Window setMinSize:ToNSSize(minSize)]; + [Window setMaxSize:ToNSSize(maxSize)]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reason) { + if (_inResize) { + return S_OK; + } + + _inResize = true; + + START_COM_CALL; + auto resizeBlock = ResizeScope(View, reason); + + @autoreleasepool { + auto maxSize = [Window maxSize]; + auto minSize = [Window minSize]; + + if (x < minSize.width) { + x = minSize.width; + } + + if (y < minSize.height) { + y = minSize.height; + } + + if (x > maxSize.width) { + x = maxSize.width; + } + + if (y > maxSize.height) { + y = maxSize.height; + } + + @try { + if (!_shown) { + BaseEvents->Resized(AvnSize{x, y}, reason); + } + + [Window setContentSize:NSSize{x, y}]; + [Window invalidateShadow]; + } + @finally { + _inResize = false; + } + + return S_OK; + } +} + +HRESULT WindowBaseImpl::Invalidate(__attribute__((unused)) AvnRect rect) { + START_COM_CALL; + + @autoreleasepool { + [View setNeedsDisplayInRect:[View frame]]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::SetMainMenu(IAvnMenu *menu) { + START_COM_CALL; + + auto nativeMenu = dynamic_cast(menu); + + auto nsmenu = nativeMenu->GetNative(); + + [Window applyMenu:nsmenu]; + + if ([Window isKeyWindow]) { + [Window showWindowMenuWithAppMenu]; + } + + return S_OK; +} + +HRESULT WindowBaseImpl::BeginMoveDrag() { + START_COM_CALL; + + @autoreleasepool { + auto lastEvent = [View lastMouseDownEvent]; + + if (lastEvent == nullptr) { + return S_OK; + } + + [Window performWindowDragWithEvent:lastEvent]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::BeginResizeDrag(__attribute__((unused)) AvnWindowEdge edge) { + START_COM_CALL; + + return S_OK; +} + +HRESULT WindowBaseImpl::GetPosition(AvnPoint *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + auto frame = [Window frame]; + + ret->X = frame.origin.x; + ret->Y = frame.origin.y + frame.size.height; + + *ret = ConvertPointY(*ret); + + return S_OK; + } +} + +HRESULT WindowBaseImpl::SetPosition(AvnPoint point) { + START_COM_CALL; + + @autoreleasepool { + lastPositionSet = point; + [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(point))]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::PointToClient(AvnPoint point, AvnPoint *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + point = ConvertPointY(point); + NSRect convertRect = [Window convertRectFromScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)]; + auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y); + + *ret = [View translateLocalPoint:ToAvnPoint(viewPoint)]; + + return S_OK; + } +} + +HRESULT WindowBaseImpl::PointToScreen(AvnPoint point, AvnPoint *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + auto cocoaViewPoint = ToNSPoint([View translateLocalPoint:point]); + NSRect convertRect = [Window convertRectToScreen:NSMakeRect(cocoaViewPoint.x, cocoaViewPoint.y, 0.0, 0.0)]; + auto cocoaScreenPoint = NSPointFromCGPoint(NSMakePoint(convertRect.origin.x, convertRect.origin.y)); + *ret = ConvertPointY(ToAvnPoint(cocoaScreenPoint)); + + return S_OK; + } +} + +HRESULT WindowBaseImpl::ThreadSafeSetSwRenderedFrame(AvnFramebuffer *fb, IUnknown *dispose) { + START_COM_CALL; + + [View setSwRenderedFrame:fb dispose:dispose]; + return S_OK; +} + +HRESULT WindowBaseImpl::SetCursor(IAvnCursor *cursor) { + START_COM_CALL; + + @autoreleasepool { + Cursor *avnCursor = dynamic_cast(cursor); + 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(*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()]; +} \ No newline at end of file diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h new file mode 100644 index 0000000000..b229921baa --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -0,0 +1,97 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#ifndef AVALONIA_NATIVE_OSX_WINDOWIMPL_H +#define AVALONIA_NATIVE_OSX_WINDOWIMPL_H + + +#import "WindowBaseImpl.h" +#include "IWindowStateChanged.h" + +class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged +{ +private: + bool _canResize; + bool _fullScreenActive; + SystemDecorations _decorations; + AvnWindowState _lastWindowState; + AvnWindowState _actualWindowState; + bool _inSetWindowState; + NSRect _preZoomSize; + bool _transitioningWindowState; + bool _isClientAreaExtended; + bool _isDialog; + AvnExtendClientAreaChromeHints _extendClientHints; + + FORWARD_IUNKNOWN() +BEGIN_INTERFACE_MAP() + INHERIT_INTERFACE_MAP(WindowBaseImpl) + INTERFACE_MAP_ENTRY(IAvnWindow, IID_IAvnWindow) + END_INTERFACE_MAP() + virtual ~WindowImpl() + { + } + + ComPtr 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 diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm new file mode 100644 index 0000000000..0d325c4490 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -0,0 +1,539 @@ +// +// Created by Dan Walmsley on 04/05/2022. +// Copyright (c) 2022 Avalonia. All rights reserved. +// + +#import +#import "window.h" +#import "AutoFitContentView.h" +#import "AvnView.h" +#include "automation.h" + +WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBaseImpl(events, gl) { + _isClientAreaExtended = false; + _extendClientHints = AvnDefaultChrome; + _fullScreenActive = false; + _canResize = true; + _decorations = SystemDecorationsFull; + _transitioningWindowState = false; + _inSetWindowState = false; + _lastWindowState = Normal; + _actualWindowState = Normal; + WindowEvents = events; + [Window setCanBecomeKeyAndMain]; + [Window disableCursorRects]; + [Window setTabbingMode:NSWindowTabbingModeDisallowed]; + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; +} + +void WindowImpl::HideOrShowTrafficLights() { + if (Window == nil) { + return; + } + + for (id subview in Window.contentView.superview.subviews) { + if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { + NSView *titlebarView = [subview subviews][0]; + for (id button in titlebarView.subviews) { + if ([button isKindOfClass:[NSButton class]]) { + if (_isClientAreaExtended) { + auto wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + [button setHidden:!wantsChrome]; + } else { + [button setHidden:(_decorations != SystemDecorationsFull)]; + } + + [button setWantsLayer:true]; + } + } + } + } +} + +HRESULT WindowImpl::Show(bool activate, bool isDialog) { + START_COM_CALL; + + @autoreleasepool { + _isDialog = isDialog; + WindowBaseImpl::Show(activate, isDialog); + + HideOrShowTrafficLights(); + + return SetWindowState(_lastWindowState); + } +} + +HRESULT WindowImpl::SetEnabled(bool enable) { + START_COM_CALL; + + @autoreleasepool { + [Window setEnabled:enable]; + return S_OK; + } +} + +HRESULT WindowImpl::SetParent(IAvnWindow *parent) { + START_COM_CALL; + + @autoreleasepool { + if (parent == nullptr) + return E_POINTER; + + auto cparent = dynamic_cast(parent); + if (cparent == nullptr) + return E_INVALIDARG; + + // If one tries to show a child window with a minimized parent window, then the parent window will be + // restored but macOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive + // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. + if (cparent->WindowState() == Minimized) + cparent->SetWindowState(Normal); + + [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenAuxiliary]; + [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; + + UpdateStyle(); + + return S_OK; + } +} + +void WindowImpl::StartStateTransition() { + _transitioningWindowState = true; +} + +void WindowImpl::EndStateTransition() { + _transitioningWindowState = false; +} + +SystemDecorations WindowImpl::Decorations() { + return _decorations; +} + +AvnWindowState WindowImpl::WindowState() { + return _lastWindowState; +} + +void WindowImpl::WindowStateChanged() { + if (_shown && !_inSetWindowState && !_transitioningWindowState) { + AvnWindowState state; + GetWindowState(&state); + + if (_lastWindowState != state) { + if (_isClientAreaExtended) { + if (_lastWindowState == FullScreen) { + // we exited fs. + if (_extendClientHints & AvnOSXThickTitleBar) { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + + [Window setTitlebarAppearsTransparent:true]; + + [StandardContainer setFrameSize:StandardContainer.frame.size]; + } else if (state == FullScreen) { + // we entered fs. + if (_extendClientHints & AvnOSXThickTitleBar) { + Window.toolbar = nullptr; + } + + [Window setTitlebarAppearsTransparent:false]; + + [StandardContainer setFrameSize:StandardContainer.frame.size]; + } + } + + _lastWindowState = state; + _actualWindowState = state; + WindowEvents->WindowStateChanged(state); + } + } +} + +bool WindowImpl::UndecoratedIsMaximized() { + auto windowSize = [Window frame]; + auto available = [Window screen].visibleFrame; + return CGRectEqualToRect(windowSize, available); +} + +bool WindowImpl::IsZoomed() { + return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized(); +} + +void WindowImpl::DoZoom() { + switch (_decorations) { + case SystemDecorationsNone: + case SystemDecorationsBorderOnly: + [Window setFrame:[Window screen].visibleFrame display:true]; + break; + + + case SystemDecorationsFull: + [Window performZoom:Window]; + break; + } +} + +HRESULT WindowImpl::SetCanResize(bool value) { + START_COM_CALL; + + @autoreleasepool { + _canResize = value; + UpdateStyle(); + return S_OK; + } +} + +HRESULT WindowImpl::SetDecorations(SystemDecorations value) { + START_COM_CALL; + + @autoreleasepool { + auto currentWindowState = _lastWindowState; + _decorations = value; + + if (_fullScreenActive) { + return S_OK; + } + + UpdateStyle(); + + HideOrShowTrafficLights(); + + switch (_decorations) { + case SystemDecorationsNone: + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + + if (currentWindowState == Maximized) { + if (!UndecoratedIsMaximized()) { + DoZoom(); + } + } + break; + + case SystemDecorationsBorderOnly: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + + if (currentWindowState == Maximized) { + if (!UndecoratedIsMaximized()) { + DoZoom(); + } + } + break; + + case SystemDecorationsFull: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + + if (currentWindowState == Maximized) { + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } + break; + } + + return S_OK; + } +} + +HRESULT WindowImpl::SetTitle(char *utf8title) { + START_COM_CALL; + + @autoreleasepool { + _lastTitle = [NSString stringWithUTF8String:(const char *) utf8title]; + [Window setTitle:_lastTitle]; + + return S_OK; + } +} + +HRESULT WindowImpl::SetTitleBarColor(AvnColor color) { + START_COM_CALL; + + @autoreleasepool { + float a = (float) color.Alpha / 255.0f; + float r = (float) color.Red / 255.0f; + float g = (float) color.Green / 255.0f; + float b = (float) color.Blue / 255.0f; + + auto nscolor = [NSColor colorWithSRGBRed:r green:g blue:b alpha:a]; + + // Based on the titlebar color we have to choose either light or dark + // OSX doesnt let you set a foreground color for titlebar. + if ((r * 0.299 + g * 0.587 + b * 0.114) > 186.0f / 255.0f) { + [Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantLight]]; + } else { + [Window setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]]; + } + + [Window setTitlebarAppearsTransparent:true]; + [Window setBackgroundColor:nscolor]; + } + + return S_OK; +} + +HRESULT WindowImpl::GetWindowState(AvnWindowState *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + if (([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) { + *ret = FullScreen; + return S_OK; + } + + if ([Window isMiniaturized]) { + *ret = Minimized; + return S_OK; + } + + if (IsZoomed()) { + *ret = Maximized; + return S_OK; + } + + *ret = Normal; + + return S_OK; + } +} + +HRESULT WindowImpl::TakeFocusFromChildren() { + START_COM_CALL; + + @autoreleasepool { + if (Window == nil) + return S_OK; + if ([Window isKeyWindow]) + [Window makeFirstResponder:View]; + + return S_OK; + } +} + +HRESULT WindowImpl::SetExtendClientArea(bool enable) { + START_COM_CALL; + + @autoreleasepool { + _isClientAreaExtended = enable; + + if (enable) { + Window.titleVisibility = NSWindowTitleHidden; + + [Window setTitlebarAppearsTransparent:true]; + + auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + if (wantsTitleBar) { + [StandardContainer ShowTitleBar:true]; + } else { + [StandardContainer ShowTitleBar:false]; + } + + if (_extendClientHints & AvnOSXThickTitleBar) { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } else { + Window.toolbar = nullptr; + } + } else { + Window.titleVisibility = NSWindowTitleVisible; + Window.toolbar = nullptr; + [Window setTitlebarAppearsTransparent:false]; + View.layer.zPosition = 0; + } + + [Window setIsExtended:enable]; + + HideOrShowTrafficLights(); + + UpdateStyle(); + + return S_OK; + } +} + +HRESULT WindowImpl::SetExtendClientAreaHints(AvnExtendClientAreaChromeHints hints) { + START_COM_CALL; + + @autoreleasepool { + _extendClientHints = hints; + + SetExtendClientArea(_isClientAreaExtended); + return S_OK; + } +} + +HRESULT WindowImpl::GetExtendTitleBarHeight(double *ret) { + START_COM_CALL; + + @autoreleasepool { + if (ret == nullptr) { + return E_POINTER; + } + + *ret = [Window getExtendedTitleBarHeight]; + + return S_OK; + } +} + +HRESULT WindowImpl::SetExtendTitleBarHeight(double value) { + START_COM_CALL; + + @autoreleasepool { + [StandardContainer SetTitleBarHeightHint:value]; + return S_OK; + } +} + +void WindowImpl::EnterFullScreenMode() { + _fullScreenActive = true; + + [Window setTitle:_lastTitle]; + [Window toggleFullScreen:nullptr]; +} + +void WindowImpl::ExitFullScreenMode() { + [Window toggleFullScreen:nullptr]; + + _fullScreenActive = false; + + SetDecorations(_decorations); +} + +HRESULT WindowImpl::SetWindowState(AvnWindowState state) { + START_COM_CALL; + + @autoreleasepool { + if (Window == nullptr) { + return S_OK; + } + + if (_actualWindowState == state) { + return S_OK; + } + + _inSetWindowState = true; + + auto currentState = _actualWindowState; + _lastWindowState = state; + + if (currentState == Normal) { + _preZoomSize = [Window frame]; + } + + if (_shown) { + switch (state) { + case Maximized: + if (currentState == FullScreen) { + ExitFullScreenMode(); + } + + lastPositionSet.X = 0; + lastPositionSet.Y = 0; + + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } + + if (!IsZoomed()) { + DoZoom(); + } + break; + + case Minimized: + if (currentState == FullScreen) { + ExitFullScreenMode(); + } else { + [Window miniaturize:Window]; + } + break; + + case FullScreen: + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } + + EnterFullScreenMode(); + break; + + case Normal: + if ([Window isMiniaturized]) { + [Window deminiaturize:Window]; + } + + if (currentState == FullScreen) { + ExitFullScreenMode(); + } + + if (IsZoomed()) { + if (_decorations == SystemDecorationsFull) { + DoZoom(); + } else { + [Window setFrame:_preZoomSize display:true]; + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } + + } + break; + } + + _actualWindowState = _lastWindowState; + WindowEvents->WindowStateChanged(_actualWindowState); + } + + + _inSetWindowState = false; + + return S_OK; + } +} + +bool WindowImpl::IsDialog() { + return _isDialog; +} + +NSWindowStyleMask WindowImpl::GetStyle() { + unsigned long s = NSWindowStyleMaskBorderless; + + switch (_decorations) { + case SystemDecorationsNone: + s = s | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsBorderOnly: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsFull: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless; + + if (_canResize) { + s = s | NSWindowStyleMaskResizable; + } + break; + } + + if ([Window parentWindow] == nullptr) { + s |= NSWindowStyleMaskMiniaturizable; + } + + if (_isClientAreaExtended) { + s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground; + } + return s; +} diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 05b129baca..14f1f6888c 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -82,18 +82,6 @@ ComPtr _events; _isHandlingSendEvent = oldHandling; } } - -// This is needed for certain embedded controls -- (BOOL) isHandlingSendEvent -{ - return _isHandlingSendEvent; -} - -- (void)setHandlingSendEvent:(BOOL)handlingSendEvent -{ - _isHandlingSendEvent = handlingSendEvent; -} - @end extern void InitializeAvnApp(IAvnApplicationEvents* events) diff --git a/native/Avalonia.Native/src/OSX/automation.h b/native/Avalonia.Native/src/OSX/automation.h index 4a12a965fd..1727d21f27 100644 --- a/native/Avalonia.Native/src/OSX/automation.h +++ b/native/Avalonia.Native/src/OSX/automation.h @@ -1,5 +1,4 @@ #import -#include "window.h" NS_ASSUME_NONNULL_BEGIN diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 7d697140c2..7a0b3b8127 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -1,7 +1,9 @@ #include "common.h" -#include "automation.h" +#import "automation.h" +#import "window.h" #include "AvnString.h" -#include "window.h" +#import "INSWindowHolder.h" +#import "AvnView.h" @interface AvnAccessibilityElement (Events) - (void) raiseChildrenChanged; diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 9186d9e15a..a90a235b9d 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -27,7 +27,7 @@ extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnApplicationCommands* CreateApplicationCommands(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); -extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); +extern void SetAppMenu(IAvnMenu *menu); extern void SetServicesMenu (IAvnMenu* menu); extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); @@ -38,7 +38,6 @@ extern NSPoint ToNSPoint (AvnPoint p); extern NSRect ToNSRect (AvnRect r); extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ConvertPointY (AvnPoint p); -extern CGFloat PrimaryDisplayHeight(); extern NSSize ToNSSize (AvnSize s); #ifdef DEBUG #define NSDebugLog(...) NSLog(__VA_ARGS__) diff --git a/native/Avalonia.Native/src/OSX/cursor.mm b/native/Avalonia.Native/src/OSX/cursor.mm index dc38294a18..8638a03531 100644 --- a/native/Avalonia.Native/src/OSX/cursor.mm +++ b/native/Avalonia.Native/src/OSX/cursor.mm @@ -1,6 +1,5 @@ #include "common.h" #include "cursor.h" -#include class CursorFactory : public ComSingleObject { diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index ea79c494d7..011f881e94 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -343,7 +343,7 @@ public: @autoreleasepool { - ::SetAppMenu(s_appTitle, appMenu); + ::SetAppMenu(appMenu); return S_OK; } } @@ -428,7 +428,3 @@ AvnPoint ConvertPointY (AvnPoint p) return p; } -CGFloat PrimaryDisplayHeight() -{ - return NSMaxY([[[NSScreen screens] firstObject] frame]); -} diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 186fcf255b..ce46ac11e0 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -31,7 +31,6 @@ private: NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem IAvnActionCallback* _callback; IAvnPredicateCallback* _predicate; - bool _isSeparator; bool _isCheckable; public: diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 2dbe76bc6d..fd74edd772 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -1,7 +1,7 @@ #include "common.h" #include "menu.h" -#include "window.h" +#import "window.h" #include "KeyTransform.h" #include #include /* For kVK_ constants, and TIS functions. */ @@ -74,8 +74,7 @@ AvnAppMenuItem::AvnAppMenuItem(bool isSeparator) { _isCheckable = false; - _isSeparator = isSeparator; - + if(isSeparator) { _native = [NSMenuItem separatorItem]; @@ -460,7 +459,7 @@ extern IAvnMenuItem* CreateAppMenuItemSeparator() static IAvnMenu* s_appMenu = nullptr; static NSMenuItem* s_appMenuItem = nullptr; -extern void SetAppMenu (NSString* appName, IAvnMenu* menu) +extern void SetAppMenu(IAvnMenu *menu) { s_appMenu = menu; diff --git a/native/Avalonia.Native/src/OSX/rendertarget.mm b/native/Avalonia.Native/src/OSX/rendertarget.mm index dc5c24e41e..266d0345d1 100644 --- a/native/Avalonia.Native/src/OSX/rendertarget.mm +++ b/native/Avalonia.Native/src/OSX/rendertarget.mm @@ -1,14 +1,10 @@ #include "common.h" #include "rendertarget.h" -#import #import #import -#include -#include #include #include -#include @interface IOSurfaceHolder : NSObject @end diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 1369ceaea0..e9b98ce9ae 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -1,30 +1,13 @@ #ifndef window_h #define window_h -class WindowBaseImpl; +#import "avalonia-native.h" -@interface AvnView : NSView --(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 +@class AvnMenu; -@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 +class WindowBaseImpl; @interface AvnWindow : NSWindow -+(void) closeAll; -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(void) setCanBecomeKeyAndMain; -(void) pollModalSession: (NSModalSession _Nonnull) session; @@ -33,45 +16,11 @@ class WindowBaseImpl; -(void) setEnabled: (bool) enable; -(void) showAppMenuOnly; -(void) showWindowMenuWithAppMenu; --(void) applyMenu:(NSMenu* _Nullable)menu; --(double) getScaling; +-(void) applyMenu:(AvnMenu* _Nullable)menu; + -(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 */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 4426e7fdff..68a459d088 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1,2105 +1,11 @@ +#import #include "common.h" -#include "window.h" -#include "KeyTransform.h" -#include "cursor.h" +#import "window.h" #include "menu.h" -#include -#include "rendertarget.h" -#include "AvnString.h" #include "automation.h" - -class WindowBaseImpl : public virtual ComObject, - public virtual IAvnWindowBase, - public INSWindowHolder -{ -private: - NSCursor* cursor; - -public: - FORWARD_IUNKNOWN() - BEGIN_INTERFACE_MAP() - INTERFACE_MAP_ENTRY(IAvnWindowBase, IID_IAvnWindowBase) - END_INTERFACE_MAP() - - virtual ~WindowBaseImpl() - { - View = NULL; - Window = NULL; - } - AutoFitContentView* StandardContainer; - AvnView* View; - AvnWindow* Window; - ComPtr BaseEvents; - ComPtr _glContext; - NSObject* renderTarget; - AvnPoint lastPositionSet; - NSString* _lastTitle; - IAvnMenu* _mainMenu; - - bool _shown; - bool _inResize; - - WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) - { - _shown = false; - _inResize = false; - _mainMenu = nullptr; - BaseEvents = events; - _glContext = gl; - renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl]; - View = [[AvnView alloc] initWithParent:this]; - StandardContainer = [[AutoFitContentView new] initWithContent:View]; - - Window = [[AvnWindow alloc] initWithParent:this]; - [Window setContentView: StandardContainer]; - - lastPositionSet.X = 100; - lastPositionSet.Y = 100; - _lastTitle = @""; - - [Window setStyleMask:NSWindowStyleMaskBorderless]; - [Window setBackingType:NSBackingStoreBuffered]; - - [Window setOpaque:false]; - } - - virtual HRESULT ObtainNSWindowHandle(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge void*)Window; - - return S_OK; - } - - virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge_retained void*)Window; - - return S_OK; - } - - virtual HRESULT ObtainNSViewHandle(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge void*)View; - - return S_OK; - } - - virtual HRESULT ObtainNSViewHandleRetained(void** ret) override - { - START_COM_CALL; - - if (ret == nullptr) - { - return E_POINTER; - } - - *ret = (__bridge_retained void*)View; - - return S_OK; - } - - virtual AvnWindow* GetNSWindow() override - { - return Window; - } - - virtual AvnView* GetNSView() override - { - return View; - } - - virtual HRESULT Show(bool activate, bool isDialog) override - { - START_COM_CALL; - - @autoreleasepool - { - SetPosition(lastPositionSet); - UpdateStyle(); - - [Window setTitle:_lastTitle]; - - if(ShouldTakeFocusOnShow() && activate) - { - [Window orderFront: Window]; - [Window makeKeyAndOrderFront:Window]; - [Window makeFirstResponder:View]; - [NSApp activateIgnoringOtherApps:YES]; - } - else - { - [Window orderFront: Window]; - } - - _shown = true; - - return S_OK; - } - } - - virtual bool ShouldTakeFocusOnShow() - { - return true; - } - - virtual HRESULT Hide () override - { - START_COM_CALL; - - @autoreleasepool - { - if(Window != nullptr) - { - [Window orderOut:Window]; - [Window restoreParentWindow]; - } - - return S_OK; - } - } - - virtual HRESULT Activate () override - { - START_COM_CALL; - - @autoreleasepool - { - if(Window != nullptr) - { - [Window makeKeyAndOrderFront:nil]; - [NSApp activateIgnoringOtherApps:YES]; - } - } - - return S_OK; - } - - virtual HRESULT SetTopMost (bool value) override - { - START_COM_CALL; - - @autoreleasepool - { - [Window setLevel: value ? NSFloatingWindowLevel : NSNormalWindowLevel]; - - return S_OK; - } - } - - virtual HRESULT Close() override - { - 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; - } - } - - virtual HRESULT GetClientSize(AvnSize* ret) override - { - 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; - } - } - - virtual HRESULT GetFrameSize(AvnSize* ret) override - { - 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; - } - } - - virtual HRESULT GetScaling (double* ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - return E_POINTER; - - if(Window == nullptr) - { - *ret = 1; - return S_OK; - } - - *ret = [Window backingScaleFactor]; - return S_OK; - } - } - - virtual HRESULT SetMinMaxSize (AvnSize minSize, AvnSize maxSize) override - { - START_COM_CALL; - - @autoreleasepool - { - [Window setMinSize: ToNSSize(minSize)]; - [Window setMaxSize: ToNSSize(maxSize)]; - - return S_OK; - } - } - - virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override - { - if(_inResize) - { - return S_OK; - } - - _inResize = true; - - START_COM_CALL; - auto resizeBlock = ResizeScope(View, reason); - - @autoreleasepool - { - auto maxSize = [Window maxSize]; - auto minSize = [Window minSize]; - - if (x < minSize.width) - { - x = minSize.width; - } - - if (y < minSize.height) - { - y = minSize.height; - } - - if (x > maxSize.width) - { - x = maxSize.width; - } - - if (y > maxSize.height) - { - y = maxSize.height; - } - - @try - { - if(!_shown) - { - BaseEvents->Resized(AvnSize{x,y}, reason); - } - - [Window setContentSize:NSSize{x,y}]; - [Window invalidateShadow]; - } - @finally - { - _inResize = false; - } - - return S_OK; - } - } - - virtual HRESULT Invalidate (AvnRect rect) override - { - START_COM_CALL; - - @autoreleasepool - { - [View setNeedsDisplayInRect:[View frame]]; - - return S_OK; - } - } - - virtual HRESULT SetMainMenu(IAvnMenu* menu) override - { - START_COM_CALL; - - _mainMenu = menu; - - auto nativeMenu = dynamic_cast(menu); - - auto nsmenu = nativeMenu->GetNative(); - - [Window applyMenu:nsmenu]; - - if ([Window isKeyWindow]) - { - [Window showWindowMenuWithAppMenu]; - } - - return S_OK; - } - - virtual HRESULT BeginMoveDrag () override - { - START_COM_CALL; - - @autoreleasepool - { - auto lastEvent = [View lastMouseDownEvent]; - - if(lastEvent == nullptr) - { - return S_OK; - } - - [Window performWindowDragWithEvent:lastEvent]; - - return S_OK; - } - } - - virtual HRESULT BeginResizeDrag (AvnWindowEdge edge) override - { - START_COM_CALL; - - return S_OK; - } - - virtual HRESULT GetPosition (AvnPoint* ret) override - { - 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; - } - } - - virtual HRESULT SetPosition (AvnPoint point) override - { - START_COM_CALL; - - @autoreleasepool - { - lastPositionSet = point; - [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(point))]; - - return S_OK; - } - } - - virtual HRESULT PointToClient (AvnPoint point, AvnPoint* ret) override - { - 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; - } - } - - virtual HRESULT PointToScreen (AvnPoint point, AvnPoint* ret) override - { - 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; - } - } - - virtual HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer* fb, IUnknown* dispose) override - { - START_COM_CALL; - - [View setSwRenderedFrame: fb dispose: dispose]; - return S_OK; - } - - virtual HRESULT SetCursor(IAvnCursor* cursor) override - { - START_COM_CALL; - - @autoreleasepool - { - Cursor* avnCursor = dynamic_cast(cursor); - this->cursor = avnCursor->GetNative(); - UpdateCursor(); - - if(avnCursor->IsHidden()) - { - [NSCursor hide]; - } - else - { - [NSCursor unhide]; - } - - return S_OK; - } - } - - virtual void UpdateCursor() - { - if (cursor != nil) - { - [cursor set]; - } - } - - virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ppv) override - { - START_COM_CALL; - - if(View == NULL) - return E_FAIL; - *ppv = [renderTarget createSurfaceRenderTarget]; - return *ppv == nil ? E_FAIL : S_OK; - } - - virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override - { - START_COM_CALL; - - if(View == NULL) - return E_FAIL; - *retOut = ::CreateNativeControlHost(View); - return S_OK; - } - - virtual HRESULT SetBlurEnabled (bool enable) override - { - START_COM_CALL; - - [StandardContainer ShowBlur:enable]; - - return S_OK; - } - - virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, - IAvnClipboard* clipboard, IAvnDndResultCallback* cb, - void* sourceHandle) override - { - 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; - } - - virtual bool IsDialog() - { - return false; - } - -protected: - virtual NSWindowStyleMask GetStyle() - { - return NSWindowStyleMaskBorderless; - } - - void UpdateStyle() - { - [Window setStyleMask: GetStyle()]; - } - -public: - virtual void OnResized () - { - - } -}; - -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 WindowEvents; - WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) - { - _isClientAreaExtended = false; - _extendClientHints = AvnDefaultChrome; - _fullScreenActive = false; - _canResize = true; - _decorations = SystemDecorationsFull; - _transitioningWindowState = false; - _inSetWindowState = false; - _lastWindowState = Normal; - _actualWindowState = Normal; - WindowEvents = events; - [Window setCanBecomeKeyAndMain]; - [Window disableCursorRects]; - [Window setTabbingMode:NSWindowTabbingModeDisallowed]; - [Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; - } - - void 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]; - } - } - } - } - } - - virtual HRESULT Show (bool activate, bool isDialog) override - { - START_COM_CALL; - - @autoreleasepool - { - _isDialog = isDialog; - WindowBaseImpl::Show(activate, isDialog); - - HideOrShowTrafficLights(); - - return SetWindowState(_lastWindowState); - } - } - - virtual HRESULT SetEnabled (bool enable) override - { - START_COM_CALL; - - @autoreleasepool - { - [Window setEnabled:enable]; - return S_OK; - } - } - - virtual HRESULT SetParent (IAvnWindow* parent) override - { - START_COM_CALL; - - @autoreleasepool - { - if(parent == nullptr) - return E_POINTER; - - auto cparent = dynamic_cast(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 StartStateTransition () override - { - _transitioningWindowState = true; - } - - void EndStateTransition () override - { - _transitioningWindowState = false; - } - - SystemDecorations Decorations () override - { - return _decorations; - } - - AvnWindowState WindowState () override - { - return _lastWindowState; - } - - void WindowStateChanged () override - { - 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 UndecoratedIsMaximized () - { - auto windowSize = [Window frame]; - auto available = [Window screen].visibleFrame; - return CGRectEqualToRect(windowSize, available); - } - - bool IsZoomed () - { - return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized(); - } - - void DoZoom() - { - switch (_decorations) - { - case SystemDecorationsNone: - case SystemDecorationsBorderOnly: - [Window setFrame:[Window screen].visibleFrame display:true]; - break; - - - case SystemDecorationsFull: - [Window performZoom:Window]; - break; - } - } - - virtual HRESULT SetCanResize(bool value) override - { - START_COM_CALL; - - @autoreleasepool - { - _canResize = value; - UpdateStyle(); - return S_OK; - } - } - - virtual HRESULT SetDecorations(SystemDecorations value) override - { - 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; - } - } - - virtual HRESULT SetTitle (char* utf8title) override - { - START_COM_CALL; - - @autoreleasepool - { - _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; - [Window setTitle:_lastTitle]; - - return S_OK; - } - } - - virtual HRESULT SetTitleBarColor(AvnColor color) override - { - 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; - } - - virtual HRESULT GetWindowState (AvnWindowState*ret) override - { - 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; - } - } - - virtual HRESULT TakeFocusFromChildren () override - { - START_COM_CALL; - - @autoreleasepool - { - if(Window == nil) - return S_OK; - if([Window isKeyWindow]) - [Window makeFirstResponder: View]; - - return S_OK; - } - } - - virtual HRESULT SetExtendClientArea (bool enable) override - { - START_COM_CALL; - - @autoreleasepool - { - _isClientAreaExtended = enable; - - if(enable) - { - Window.titleVisibility = NSWindowTitleHidden; - - [Window setTitlebarAppearsTransparent:true]; - - auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - - if (wantsTitleBar) - { - [StandardContainer ShowTitleBar:true]; - } - else - { - [StandardContainer ShowTitleBar:false]; - } - - if(_extendClientHints & AvnOSXThickTitleBar) - { - Window.toolbar = [NSToolbar new]; - Window.toolbar.showsBaselineSeparator = false; - } - else - { - Window.toolbar = nullptr; - } - } - else - { - Window.titleVisibility = NSWindowTitleVisible; - Window.toolbar = nullptr; - [Window setTitlebarAppearsTransparent:false]; - View.layer.zPosition = 0; - } - - [Window setIsExtended:enable]; - - HideOrShowTrafficLights(); - - UpdateStyle(); - - return S_OK; - } - } - - virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override - { - START_COM_CALL; - - @autoreleasepool - { - _extendClientHints = hints; - - SetExtendClientArea(_isClientAreaExtended); - return S_OK; - } - } - - virtual HRESULT GetExtendTitleBarHeight (double*ret) override - { - START_COM_CALL; - - @autoreleasepool - { - if(ret == nullptr) - { - return E_POINTER; - } - - *ret = [Window getExtendedTitleBarHeight]; - - return S_OK; - } - } - - virtual HRESULT SetExtendTitleBarHeight (double value) override - { - START_COM_CALL; - - @autoreleasepool - { - [StandardContainer SetTitleBarHeightHint:value]; - return S_OK; - } - } - - void EnterFullScreenMode () - { - _fullScreenActive = true; - - [Window setTitle:_lastTitle]; - [Window toggleFullScreen:nullptr]; - } - - void ExitFullScreenMode () - { - [Window toggleFullScreen:nullptr]; - - _fullScreenActive = false; - - SetDecorations(_decorations); - } - - virtual HRESULT SetWindowState (AvnWindowState state) override - { - 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; - } - } - - virtual void OnResized () override - { - if(_shown && !_inSetWindowState && !_transitioningWindowState) - { - WindowStateChanged(); - } - } - - virtual bool IsDialog() override - { - return _isDialog; - } - -protected: - virtual NSWindowStyleMask GetStyle() override - { - unsigned long s = NSWindowStyleMaskBorderless; - - switch (_decorations) - { - case SystemDecorationsNone: - s = s | NSWindowStyleMaskFullSizeContentView; - break; - - case SystemDecorationsBorderOnly: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; - break; - - case SystemDecorationsFull: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless; - - if(_canResize) - { - s = s | NSWindowStyleMaskResizable; - } - break; - } - - if([Window parentWindow] == nullptr) - { - s |= NSWindowStyleMaskMiniaturizable; - } - - if(_isClientAreaExtended) - { - s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground; - } - return s; - } -}; - -NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; - -@implementation AutoFitContentView -{ - NSVisualEffectView* _titleBarMaterial; - NSBox* _titleBarUnderline; - NSView* _content; - NSVisualEffectView* _blurBehind; - double _titleBarHeightHint; - bool _settingSize; -} - --(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content -{ - _titleBarHeightHint = -1; - _content = content; - _settingSize = false; - - [self setAutoresizesSubviews:true]; - [self setWantsLayer:true]; - - _titleBarMaterial = [NSVisualEffectView new]; - [_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow]; - [_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar]; - [_titleBarMaterial setWantsLayer:true]; - _titleBarMaterial.hidden = true; - - _titleBarUnderline = [NSBox new]; - _titleBarUnderline.boxType = NSBoxSeparator; - _titleBarUnderline.fillColor = [NSColor underPageBackgroundColor]; - _titleBarUnderline.hidden = true; - - [self addSubview:_titleBarMaterial]; - [self addSubview:_titleBarUnderline]; - - _blurBehind = [NSVisualEffectView new]; - [_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; - [_blurBehind setMaterial:NSVisualEffectMaterialLight]; - [_blurBehind setWantsLayer:true]; - _blurBehind.hidden = true; - - [_blurBehind setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; - [_content setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; - - [self addSubview:_blurBehind]; - [self addSubview:_content]; - - [self setWantsLayer:true]; - return self; -} - --(void) ShowBlur:(bool)show -{ - _blurBehind.hidden = !show; -} - --(void) ShowTitleBar: (bool) show -{ - _titleBarMaterial.hidden = !show; - _titleBarUnderline.hidden = !show; -} - --(void) SetTitleBarHeightHint: (double) height -{ - _titleBarHeightHint = height; - - [self setFrameSize:self.frame.size]; -} - --(void)setFrameSize:(NSSize)newSize -{ - if(_settingSize) - { - return; - } - - _settingSize = true; - [super setFrameSize:newSize]; - - auto window = objc_cast([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; -} - --(void) SetContent: (NSView* _Nonnull) content -{ - if(content != nullptr) - { - [content removeFromSuperview]; - [self addSubview:content]; - _content = content; - } -} -@end - -@implementation AvnView -{ - ComPtr _parent; - ComPtr _swRenderedFrame; - AvnFramebuffer _swRenderedFrameBuffer; - bool _queuedDisplayFromThread; - NSTrackingArea* _area; - bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver; - AvnInputModifiers _modifierState; - NSEvent* _lastMouseDownEvent; - bool _lastKeyHandled; - AvnPixelSize _lastPixelSize; - NSObject* _renderTarget; - AvnPlatformResizeReason _resizeReason; - AvnAccessibilityElement* _accessibilityChild; -} - -- (void)onClosed -{ - @synchronized (self) - { - _parent = nullptr; - } -} - --(AvnPixelSize) getPixelSize -{ - return _lastPixelSize; -} - -- (NSEvent*) lastMouseDownEvent -{ - return _lastMouseDownEvent; -} - -- (void) updateRenderTarget -{ - [_renderTarget resize:_lastPixelSize withScale: [[self window] backingScaleFactor]]; - [self setNeedsDisplayInRect:[self frame]]; -} - --(AvnView*) initWithParent: (WindowBaseImpl*) parent -{ - self = [super init]; - _renderTarget = parent->renderTarget; - [self setWantsLayer:YES]; - [self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize]; - - _parent = parent; - _area = nullptr; - _lastPixelSize.Height = 100; - _lastPixelSize.Width = 100; - [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; - - _modifierState = AvnInputModifiersNone; - return self; -} - -- (BOOL)isFlipped -{ - return YES; -} - -- (BOOL)wantsUpdateLayer -{ - return YES; -} - -- (void)setLayer:(CALayer *)layer -{ - [_renderTarget setNewLayer: layer]; - [super setLayer: layer]; -} - -- (BOOL)isOpaque -{ - return YES; -} - -- (BOOL)acceptsFirstResponder -{ - return true; -} - -- (BOOL)acceptsFirstMouse:(NSEvent *)event -{ - return true; -} - -- (BOOL)canBecomeKeyView -{ - return true; -} - --(void)setFrameSize:(NSSize)newSize -{ - [super setFrameSize:newSize]; - - if(_area != nullptr) - { - [self removeTrackingArea:_area]; - _area = nullptr; - } - - if (_parent == nullptr) - { - return; - } - - NSRect rect = NSZeroRect; - rect.size = newSize; - - NSTrackingAreaOptions options = NSTrackingActiveAlways | NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingEnabledDuringMouseDrag; - _area = [[NSTrackingArea alloc] initWithRect:rect options:options owner:self userInfo:nullptr]; - [self addTrackingArea:_area]; - - _parent->UpdateCursor(); - - auto fsize = [self convertSizeToBacking: [self frame].size]; - - if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height) - { - _lastPixelSize.Width = (int)fsize.width; - _lastPixelSize.Height = (int)fsize.height; - [self updateRenderTarget]; - - auto reason = [self inLiveResize] ? ResizeUser : _resizeReason; - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason); - } -} - -- (void)updateLayer -{ - AvnInsidePotentialDeadlock deadlock; - if (_parent == nullptr) - { - return; - } - - _parent->BaseEvents->RunRenderPriorityJobs(); - - if (_parent == nullptr) - { - return; - } - - _parent->BaseEvents->Paint(); -} - -- (void)drawRect:(NSRect)dirtyRect -{ - return; -} - --(void) setSwRenderedFrame: (AvnFramebuffer*) fb dispose: (IUnknown*) dispose -{ - @autoreleasepool { - [_renderTarget setSwFrame:fb]; - dispose->Release(); - } -} - -- (AvnPoint) translateLocalPoint:(AvnPoint)pt -{ - pt.Y = [self bounds].size.height - pt.Y; - return pt; -} - -+ (AvnPoint)toAvnPoint:(CGPoint)p -{ - AvnPoint result; - - result.X = p.x; - result.Y = p.y; - - return result; -} - -- (void) viewDidChangeBackingProperties -{ - auto fsize = [self convertSizeToBacking: [self frame].size]; - _lastPixelSize.Width = (int)fsize.width; - _lastPixelSize.Height = (int)fsize.height; - [self updateRenderTarget]; - - if(_parent != nullptr) - { - _parent->BaseEvents->ScalingChanged([_parent->Window backingScaleFactor]); - } - - [super viewDidChangeBackingProperties]; -} - -- (bool) ignoreUserInput:(bool)trigerInputWhenDisabled -{ - auto parentWindow = objc_cast([self window]); - - if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) - { - if(trigerInputWhenDisabled) - { - auto window = dynamic_cast(_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; - - 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]; - } - - auto timestamp = [event timestamp] * 1000; - auto modifiers = [self getModifiers:[event modifierFlags]]; - - if(type != AvnRawMouseEventType::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; - } -} - -- (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; - } -} - -- (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 -{ - _isMouseOver = true; - [super mouseEntered:event]; -} - -- (void)mouseExited:(NSEvent *)event -{ - _isMouseOver = false; - [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]]; - - auto timestamp = [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 *)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; - - return result; -} - -- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )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 = 0; - - // Ensure that the managed part didn't add any new effects - reffects = (int)effects & (int)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 )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender]; -} - -- (NSDragOperation)draggingUpdated:(id )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender]; -} - -- (void)draggingExited:(id )sender -{ - [self triggerAvnDragEvent: AvnDragEventType::Leave info:sender]; -} - -- (BOOL)prepareForDragOperation:(id )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone; -} - -- (BOOL)performDragOperation:(id )sender -{ - return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone; -} - -- (void)concludeDragOperation:(nullable id )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 - +#import "WindowBaseImpl.h" +#include "WindowImpl.h" +#include "AvnView.h" @implementation AvnWindow { @@ -2109,9 +15,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _isEnabled; bool _isExtended; AvnMenu* _menu; - double _lastScaling; - IAvnAutomationPeer* _automationPeer; - NSMutableArray* _automationChildren; } -(void) setIsExtended:(bool)value; @@ -2124,11 +27,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return _parent->IsDialog(); } --(double) getScaling -{ - return _lastScaling; -} - -(double) getExtendedTitleBarHeight { if(_isExtended) @@ -2151,11 +49,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } -+(void)closeAll -{ - [[NSApplication sharedApplication] terminate:self]; -} - - (void)performClose:(id)sender { if([[self delegate] respondsToSelector:@selector(windowShouldClose:)]) @@ -2233,10 +126,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [NSApp setMenu:nativeAppMenu->GetNative()]; } - else - { - [NSApp setMenu:nullptr]; - } } -(void) applyMenu:(AvnMenu *)menu @@ -2263,7 +152,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _closed = false; _isEnabled = true; - _lastScaling = [self backingScaleFactor]; + [self backingScaleFactor]; [self setOpaque:NO]; [self setBackgroundColor: [NSColor clearColor]]; _isExtended = false; @@ -2284,7 +173,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)windowDidChangeBackingProperties:(NSNotification *)notification { - _lastScaling = [self backingScaleFactor]; + [self backingScaleFactor]; } - (void)windowWillClose:(NSNotification *)notification @@ -2501,9 +390,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { auto avnPoint = [AvnView toAvnPoint:windowPoint]; auto point = [self translateLocalPoint:avnPoint]; - AvnVector delta; + AvnVector delta = { 0, 0 }; - _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, static_cast([event timestamp] * 1000), AvnInputModifiersNone, point, delta); } } break; diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 6539cdaee6..7ebb87094a 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -20,12 +20,12 @@ namespace ControlCatalog public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { - Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent.xaml") + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml") }; public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { - Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default.xaml") + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml") }; public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index ec34193f8c..c0c83d6a35 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -3,27 +3,77 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker" + mc:Ignorable="d" + d:DesignWidth="800" + d:DesignHeight="450" x:Class="ControlCatalog.Pages.ColorPickerPage"> - - + + + + + + + + + + + + + + - + + - + ColorComponent="Alpha" + ColorModel="Hsva" + Orientation="Vertical" + HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" /> + + diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index 64e80a8e11..9f2fbb88f3 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -86,6 +86,16 @@ + + + + + + Inline Items + Inline Item 2 + Inline Item 3 + Inline Item 4 + WrapSelection diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 513b3f2424..1758c45650 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -40,19 +40,16 @@ namespace Avalonia.Controls control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - IResourceHost? current = control; + IResourceNode? current = control; while (current != null) { - if (current is IResourceHost host) + if (current.TryGetResource(key, out value)) { - if (host.TryGetResource(key, out value)) - { - return true; - } + return true; } - current = (current as IStyledElement)?.StylingParent as IResourceHost; + current = (current as IStyledElement)?.StylingParent as IResourceNode; } value = null; diff --git a/src/Avalonia.Base/Logging/ILogSink.cs b/src/Avalonia.Base/Logging/ILogSink.cs index 27558ba0ee..60709776c6 100644 --- a/src/Avalonia.Base/Logging/ILogSink.cs +++ b/src/Avalonia.Base/Logging/ILogSink.cs @@ -26,57 +26,6 @@ namespace Avalonia.Logging object? source, string messageTemplate); - /// - /// Logs an event. - /// - /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// Message property value. - void Log( - LogEventLevel level, - string area, - object? source, - string messageTemplate, - T0 propertyValue0); - - /// - /// Logs an event. - /// - /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// Message property value. - /// Message property value. - void Log( - LogEventLevel level, - string area, - object? source, - string messageTemplate, - T0 propertyValue0, - T1 propertyValue1); - - /// - /// Logs an event. - /// - /// The log event level. - /// The area that the event originates. - /// The object from which the event originates. - /// The message template. - /// Message property value. - /// Message property value. - /// Message property value. - void Log( - LogEventLevel level, - string area, - object? source, - string messageTemplate, - T0 propertyValue0, - T1 propertyValue1, - T2 propertyValue2); - /// /// Logs a new event. /// diff --git a/src/Avalonia.Base/Logging/TraceLogSink.cs b/src/Avalonia.Base/Logging/TraceLogSink.cs index 02ed191d2c..05e4b8bc5a 100644 --- a/src/Avalonia.Base/Logging/TraceLogSink.cs +++ b/src/Avalonia.Base/Logging/TraceLogSink.cs @@ -28,31 +28,7 @@ namespace Avalonia.Logging { if (IsEnabled(level, area)) { - Trace.WriteLine(Format(area, messageTemplate, source)); - } - } - - public void Log(LogEventLevel level, string area, object? source, string messageTemplate, T0 propertyValue0) - { - if (IsEnabled(level, area)) - { - Trace.WriteLine(Format(area, messageTemplate, source, propertyValue0)); - } - } - - public void Log(LogEventLevel level, string area, object? source, string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - if (IsEnabled(level, area)) - { - Trace.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1)); - } - } - - public void Log(LogEventLevel level, string area, object? source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - if (IsEnabled(level, area)) - { - Trace.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1, propertyValue2)); + Trace.WriteLine(Format(area, messageTemplate, source, null)); } } @@ -68,9 +44,7 @@ namespace Avalonia.Logging string area, string template, object? source, - T0? v0 = default, - T1? v1 = default, - T2? v2 = default) + object?[]? values) { var result = new StringBuilder(template.Length); var r = new CharacterReader(template.AsSpan()); @@ -93,13 +67,7 @@ namespace Avalonia.Logging if (r.Peek != '{') { result.Append('\''); - result.Append(i++ switch - { - 0 => v0, - 1 => v1, - 2 => v2, - _ => null - }); + result.Append(values?[i++]); result.Append('\''); r.TakeUntil('}'); r.Take(); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7f0f204886..4205268bc6 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -528,7 +528,7 @@ namespace Avalonia.Media.TextFormatting /// Creates an empty text line. /// /// The empty text line. - public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, TextParagraphProperties paragraphProperties) + public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties) { var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; @@ -542,7 +542,7 @@ namespace Avalonia.Media.TextFormatting var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) }; - return new TextLineImpl(textRuns, firstTextSourceIndex, 0, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine(); + return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); } /// @@ -561,7 +561,7 @@ namespace Avalonia.Media.TextFormatting { if(textRuns.Count == 0) { - return CreateEmptyTextLine(firstTextSourceIndex, paragraphProperties); + return CreateEmptyTextLine(firstTextSourceIndex,paragraphWidth, paragraphProperties); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 0df608cb34..5d5d45db2d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -410,7 +410,7 @@ namespace Avalonia.Media.TextFormatting { if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); Bounds = new Rect(0,0,0, textLine.Height); @@ -434,7 +434,7 @@ namespace Avalonia.Media.TextFormatting { if(previousLine != null && previousLine.NewLineLength > 0) { - var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, _paragraphProperties); + var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties); textLines.Add(emptyTextLine); @@ -483,7 +483,7 @@ namespace Avalonia.Media.TextFormatting //Make sure the TextLayout always contains at least on empty line if(textLines.Count == 0) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); textLines.Add(textLine); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 6a704f6f3e..73ec055bbe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -252,7 +252,7 @@ namespace Avalonia.Media.TextFormatting //Look at the left and right edge of the current run if (currentRun.IsLeftToRight) { - if (lastRun == null || lastRun.IsLeftToRight) + if (_flowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight)) { if (characterIndex <= textRun.Text.Start) { @@ -418,11 +418,6 @@ namespace Avalonia.Media.TextFormatting continue; } - if(currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex) - { - continue; - } - TextRun? nextRun = null; if (index + 1 < TextRuns.Count) @@ -460,7 +455,7 @@ namespace Avalonia.Media.TextFormatting } default: { - goto noop; + goto noop; } } @@ -541,6 +536,11 @@ namespace Avalonia.Media.TextFormatting endX += currentRun.Size.Width; } + if (currentPosition < firstTextSourceCharacterIndex) + { + startX += currentRun.Size.Width; + } + currentPosition += currentRun.TextSourceLength; break; @@ -554,24 +554,29 @@ namespace Avalonia.Media.TextFormatting var width = endX - startX; - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + if (!MathUtilities.IsZero(width)) { - currentRect = currentRect.WithWidth(currentRect.Width + width); + if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + { + currentRect = currentRect.WithWidth(currentRect.Width + width); - var textBounds = new TextBounds(currentRect, currentDirection); + var textBounds = new TextBounds(currentRect, currentDirection); - result[result.Count - 1] = textBounds; - } - else - { - currentRect = new Rect(startX, 0, width, Height); + result[result.Count - 1] = textBounds; + } + else + { - result.Add(new TextBounds(currentRect, currentDirection)); + currentRect = new Rect(startX, 0, width, Height); + + result.Add(new TextBounds(currentRect, currentDirection)); + + } } if (currentDirection == FlowDirection.LeftToRight) { - if (currentPosition >= firstTextSourceCharacterIndex + textLength) + if (currentPosition > firstTextSourceCharacterIndex + textLength) { break; } @@ -595,10 +600,10 @@ namespace Avalonia.Media.TextFormatting public TextLineImpl FinalizeLine() { - BidiReorder(); - _textLineMetrics = CreateLineMetrics(); + BidiReorder(); + return this; } @@ -1026,7 +1031,7 @@ namespace Avalonia.Media.TextFormatting var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight; - + var width = 0d; var widthIncludingWhitespace = 0d; var trailingWhitespaceLength = 0; @@ -1036,8 +1041,8 @@ namespace Avalonia.Media.TextFormatting var lineGap = glyphTypeface.LineGap * scale; var height = descent - ascent + lineGap; - - var lineHeight = _paragraphProperties.LineHeight; + + var lineHeight = _paragraphProperties.LineHeight; for (var index = 0; index < _textRuns.Count; index++) { @@ -1073,41 +1078,11 @@ namespace Avalonia.Media.TextFormatting } } - switch (_paragraphProperties.FlowDirection) + if (index == _textRuns.Count - 1) { - case FlowDirection.LeftToRight: - { - if (index == _textRuns.Count - 1) - { - width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; - trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength = textRun.GlyphRun.Metrics.NewlineLength; - } - - break; - } - - case FlowDirection.RightToLeft: - { - if (index == _textRuns.Count - 1) - { - var firstRun = _textRuns[0]; - - if (firstRun is ShapedTextCharacters shapedTextCharacters) - { - var offset = shapedTextCharacters.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - - shapedTextCharacters.GlyphRun.Metrics.Width; - - width = widthIncludingWhitespace + - textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset; - - trailingWhitespaceLength = shapedTextCharacters.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength = shapedTextCharacters.GlyphRun.Metrics.NewlineLength; - } - } - - break; - } + width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; + trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; + newLineLength = textRun.GlyphRun.Metrics.NewlineLength; } widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace; @@ -1166,10 +1141,10 @@ namespace Avalonia.Media.TextFormatting if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) { - if(lineHeight > height) + if (lineHeight > height) { height = lineHeight; - } + } } return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start, diff --git a/src/Avalonia.Base/Styling/IStyle.cs b/src/Avalonia.Base/Styling/IStyle.cs index 78fbe0f2b5..738a69cb88 100644 --- a/src/Avalonia.Base/Styling/IStyle.cs +++ b/src/Avalonia.Base/Styling/IStyle.cs @@ -8,7 +8,7 @@ namespace Avalonia.Styling /// /// Defines the interface for styles. /// - public interface IStyle + public interface IStyle : IResourceNode { /// /// Gets a collection of child styles. diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 81502f1570..d79081152e 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -160,7 +160,7 @@ namespace Avalonia.Styling for (var i = Count - 1; i >= 0; --i) { - if (this[i] is IResourceProvider p && p.TryGetResource(key, out value)) + if (this[i].TryGetResource(key, out value)) { return true; } diff --git a/src/Avalonia.Base/Utilities/WeakHashList.cs b/src/Avalonia.Base/Utilities/WeakHashList.cs index df480aa062..fe582e8a78 100644 --- a/src/Avalonia.Base/Utilities/WeakHashList.cs +++ b/src/Avalonia.Base/Utilities/WeakHashList.cs @@ -118,7 +118,7 @@ internal class WeakHashList where T : class { if (_arr != null) { - for (var c = 0; c < _arr.Length; c++) + for (var c = 0; c < _arrCount; c++) { if (_arr[c]?.TryGetTarget(out var target) == true && target == item) { diff --git a/src/Avalonia.Controls.ColorPicker/ColorComponent.cs b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs new file mode 100644 index 0000000000..71725056cf --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs @@ -0,0 +1,28 @@ +namespace Avalonia.Controls +{ + /// + /// Defines a specific component within a color model. + /// + public enum ColorComponent + { + /// + /// Represents the alpha component. + /// + Alpha = 0, + + /// + /// Represents the first color component which is Red when RGB or Hue when HSV. + /// + Component1 = 1, + + /// + /// Represents the second color component which is Green when RGB or Saturation when HSV. + /// + Component2 = 2, + + /// + /// Represents the third color component which is Blue when RGB or Value when HSV. + /// + Component3 = 3 + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorModel.cs b/src/Avalonia.Controls.ColorPicker/ColorModel.cs new file mode 100644 index 0000000000..f11b514706 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorModel.cs @@ -0,0 +1,18 @@ +namespace Avalonia.Controls +{ + /// + /// Defines the model used to represent colors. + /// + public enum ColorModel + { + /// + /// Color is represented by hue, saturation, value and alpha components. + /// + Hsva, + + /// + /// Color is represented by red, green, blue and alpha components. + /// + Rgba + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs new file mode 100644 index 0000000000..0fa6ab8083 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -0,0 +1,50 @@ +using Avalonia.Data; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + public partial class ColorPreviewer + { + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.Transparent.ToHsv(), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowAccentColorsProperty = + AvaloniaProperty.Register( + nameof(ShowAccentColors), + true); + + /// + /// Gets or sets the currently previewed color in the HSV color model. + /// + /// + /// 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. + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + /// + /// Gets or sets a value indicating whether accent colors are shown along + /// with the preview color. + /// + public bool ShowAccentColors + { + get => GetValue(ShowAccentColorsProperty); + set => SetValue(ShowAccentColorsProperty, value); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs new file mode 100644 index 0000000000..d04ddf4bd6 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -0,0 +1,130 @@ +using System; +using System.Globalization; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives.Converters; +using Avalonia.Input; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Presents a preview color with optional accent colors. + /// + [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 + { + /// + /// Event for when the selected color changes within the previewer. + /// This occurs when an accent color is pressed. + /// + public event EventHandler? ColorChanged; + + private bool eventsConnected = false; + + private Border? AccentDec1Border; + private Border? AccentDec2Border; + private Border? AccentInc1Border; + private Border? AccentInc2Border; + + /// + /// Initializes a new instance of the class. + /// + public ColorPreviewer() : base() + { + } + + /// + /// Connects or disconnects all control event handlers. + /// + /// True to connect event handlers, otherwise false. + 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; + } + } + + /// + 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(nameof(AccentDec1Border)); + AccentDec2Border = e.NameScope.Find(nameof(AccentDec2Border)); + AccentInc1Border = e.NameScope.Find(nameof(AccentInc1Border)); + AccentInc2Border = e.NameScope.Find(nameof(AccentInc2Border)); + + // Must connect after controls are found + ConnectEvents(true); + + base.OnApplyTemplate(e); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == HsvColorProperty) + { + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue().ToRgb(), + change.GetNewValue().ToRgb())); + } + + base.OnPropertyChanged(change); + } + + /// + /// Called before the event occurs. + /// + /// The defining old/new colors. + protected virtual void OnColorChanged(ColorChangedEventArgs e) + { + ColorChanged?.Invoke(this, e); + } + + /// + /// Event handler for when an accent color border is pressed. + /// This will update the color to the background of the pressed panel. + /// + 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())); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs new file mode 100644 index 0000000000..31bd296288 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -0,0 +1,146 @@ +using Avalonia.Data; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + public partial class ColorSlider + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorComponentProperty = + AvaloniaProperty.Register( + nameof(ColorComponent), + ColorComponent.Component1); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorModelProperty = + AvaloniaProperty.Register( + nameof(ColorModel), + ColorModel.Rgba); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv(), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaMaxForcedProperty = + AvaloniaProperty.Register( + nameof(IsAlphaMaxForced), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAutoUpdatingEnabledProperty = + AvaloniaProperty.Register( + nameof(IsAutoUpdatingEnabled), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsSaturationValueMaxForcedProperty = + AvaloniaProperty.Register( + nameof(IsSaturationValueMaxForced), + true); + + /// + /// Gets or sets the currently selected color in the RGB color model. + /// + /// + /// Use this property instead of when in + /// to avoid loss of precision and color drifting. + /// + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + /// Gets or sets the color component represented by the slider. + /// + public ColorComponent ColorComponent + { + get => GetValue(ColorComponentProperty); + set => SetValue(ColorComponentProperty, value); + } + + /// + /// Gets or sets the active color model used by the slider. + /// + public ColorModel ColorModel + { + get => GetValue(ColorModelProperty); + set => SetValue(ColorModelProperty, value); + } + + /// + /// Gets or sets the currently selected color in the HSV color model. + /// + /// + /// Use this property instead of when in + /// to avoid loss of precision and color drifting. + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + /// + /// Gets or sets a value indicating whether the alpha component is always forced to maximum for components + /// other than . + /// This ensures that the background is always visible and never transparent regardless of the actual color. + /// + public bool IsAlphaMaxForced + { + get => GetValue(IsAlphaMaxForcedProperty); + set => SetValue(IsAlphaMaxForcedProperty, value); + } + + /// + /// Gets or sets a value indicating whether automatic background and foreground updates will be + /// calculated when the set color changes. + /// + /// + /// This can be disabled for performance reasons when working with multiple sliders. + /// + public bool IsAutoUpdatingEnabled + { + get => GetValue(IsAutoUpdatingEnabledProperty); + set => SetValue(IsAutoUpdatingEnabledProperty, value); + } + + /// + /// 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 will be changed. + /// This ensures, for example, that the Hue background is always visible and never washed out regardless of the actual color. + /// + public bool IsSaturationValueMaxForced + { + get => GetValue(IsSaturationValueMaxForcedProperty); + set => SetValue(IsSaturationValueMaxForcedProperty, value); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs new file mode 100644 index 0000000000..3c38c6ed1b --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -0,0 +1,399 @@ +using System; +using Avalonia.Controls.Metadata; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Primitives +{ + /// + /// A slider with a background that represents a single color component. + /// + [PseudoClasses(pcDarkSelector, pcLightSelector)] + public partial class ColorSlider : Slider + { + protected const string pcDarkSelector = ":dark-selector"; + protected const string pcLightSelector = ":light-selector"; + + /// + /// Event for when the selected color changes within the slider. + /// + public event EventHandler? ColorChanged; + + private const double MaxHue = 359.99999999999999999; // 17 decimal places + private bool disableUpdates = false; + + /// + /// Initializes a new instance of the class. + /// + public ColorSlider() : base() + { + } + + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// + 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); + } + } + } + + /// + /// Generates a new background image for the color slider and applies it. + /// + 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)); + } + } + } + + /// + /// Updates the slider property values by applying the current color. + /// + /// + /// Warning: This will trigger property changed updates. + /// Consider using externally. + /// + 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; + } + } + } + + /// + /// Gets the current color determined by the slider values. + /// + 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()); + } + } + + /// + /// 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. + /// + /// The actual color to get the equivalent background color for. + /// The equivalent, perceived background color. + 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; + } + } + + /// + /// 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. + /// + /// The actual color to get the equivalent background color for. + /// The equivalent, perceived background color. + 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; + } + + /// + 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(), + change.GetNewValue())); + + disableUpdates = false; + } + else if (change.Property == HsvColorProperty) + { + disableUpdates = true; + + Color = HsvColor.ToRgb(); + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue().ToRgb(), + change.GetNewValue().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); + } + + /// + /// Called before the event occurs. + /// + /// The defining old/new colors. + protected virtual void OnColorChanged(ColorChangedEventArgs e) + { + ColorChanged?.Invoke(this, e); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs deleted file mode 100644 index b912d39aba..0000000000 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs +++ /dev/null @@ -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 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; - } - - /// - /// Gets the relative (perceptual) luminance/brightness of the given color. - /// 1 is closer to white while 0 is closer to black. - /// - /// The color to calculate relative luminance for. - /// The relative (perceptual) luminance/brightness of the given color. - 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); - } - } -} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 824bf9ab05..587a89ee38 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -3,6 +3,7 @@ // // Licensed to The Avalonia Project under the MIT License. +using Avalonia.Data; using Avalonia.Media; namespace Avalonia.Controls.Primitives @@ -10,6 +11,88 @@ namespace Avalonia.Controls.Primitives /// public partial class ColorSpectrum { + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ComponentsProperty = + AvaloniaProperty.Register( + nameof(Components), + ColorSpectrumComponents.HueSaturation); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv(), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxHueProperty = + AvaloniaProperty.Register( + nameof(MaxHue), + 359); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxSaturationProperty = + AvaloniaProperty.Register( + nameof(MaxSaturation), + 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxValueProperty = + AvaloniaProperty.Register( + nameof(MaxValue), + 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinHueProperty = + AvaloniaProperty.Register( + nameof(MinHue), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinSaturationProperty = + AvaloniaProperty.Register( + nameof(MinSaturation), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinValueProperty = + AvaloniaProperty.Register( + nameof(MinValue), + 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShapeProperty = + AvaloniaProperty.Register( + nameof(Shape), + ColorSpectrumShape.Box); + /// /// Gets or sets the currently selected color in the RGB color model. /// @@ -23,14 +106,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ColorProperty = - AvaloniaProperty.Register( - nameof(Color), - Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)); - /// /// Gets or sets the two HSV color components displayed by the spectrum. /// @@ -43,14 +118,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ComponentsProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ComponentsProperty = - AvaloniaProperty.Register( - nameof(Components), - ColorSpectrumComponents.HueSaturation); - /// /// Gets or sets the currently selected color in the HSV color model. /// @@ -65,14 +132,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(HsvColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty HsvColorProperty = - AvaloniaProperty.Register( - nameof(HsvColor), - new HsvColor(1, 0, 0, 1)); - /// /// Gets or sets the maximum value of the Hue component in the range from 0..359. /// This property must be greater than . @@ -86,12 +145,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MaxHueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MaxHueProperty = - AvaloniaProperty.Register(nameof(MaxHue), 359); - /// /// Gets or sets the maximum value of the Saturation component in the range from 0..100. /// This property must be greater than . @@ -105,12 +158,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MaxSaturationProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MaxSaturationProperty = - AvaloniaProperty.Register(nameof(MaxSaturation), 100); - /// /// Gets or sets the maximum value of the Value component in the range from 0..100. /// This property must be greater than . @@ -124,12 +171,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MaxValueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MaxValueProperty = - AvaloniaProperty.Register(nameof(MaxValue), 100); - /// /// Gets or sets the minimum value of the Hue component in the range from 0..359. /// This property must be less than . @@ -143,12 +184,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MinHueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MinHueProperty = - AvaloniaProperty.Register(nameof(MinHue), 0); - /// /// Gets or sets the minimum value of the Saturation component in the range from 0..100. /// This property must be less than . @@ -162,12 +197,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MinSaturationProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MinSaturationProperty = - AvaloniaProperty.Register(nameof(MinSaturation), 0); - /// /// Gets or sets the minimum value of the Value component in the range from 0..100. /// This property must be less than . @@ -181,12 +210,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MinValueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MinValueProperty = - AvaloniaProperty.Register(nameof(MinValue), 0); - /// /// Gets or sets the displayed shape of the spectrum. /// @@ -195,13 +218,5 @@ namespace Avalonia.Controls.Primitives get => GetValue(ShapeProperty); set => SetValue(ShapeProperty, value); } - - /// - /// Defines the property. - /// - public static readonly StyledProperty ShapeProperty = - AvaloniaProperty.Register( - nameof(Shape), - ColorSpectrumShape.Box); } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index fe9a2fac43..245592207e 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; @@ -20,7 +21,6 @@ namespace Avalonia.Controls.Primitives /// /// A two dimensional spectrum for color selection. /// - [TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))] [TemplatePart("PART_InputTarget", typeof(Canvas))] [TemplatePart("PART_LayoutRoot", typeof(Panel))] [TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))] @@ -29,10 +29,11 @@ namespace Avalonia.Controls.Primitives [TemplatePart("PART_SpectrumRectangle", typeof(Rectangle))] [TemplatePart("PART_SpectrumOverlayEllipse", typeof(Ellipse))] [TemplatePart("PART_SpectrumOverlayRectangle", typeof(Rectangle))] - [PseudoClasses(pcPressed, pcLargeSelector, pcLightSelector)] + [PseudoClasses(pcPressed, pcLargeSelector, pcDarkSelector, pcLightSelector)] public partial class ColorSpectrum : TemplatedControl { protected const string pcPressed = ":pressed"; + protected const string pcDarkSelector = ":dark-selector"; protected const string pcLargeSelector = ":large-selector"; protected const string pcLightSelector = ":light-selector"; @@ -60,7 +61,6 @@ namespace Avalonia.Controls.Primitives private Ellipse? _spectrumOverlayEllipse; private Canvas? _inputTarget; private Panel? _selectionEllipsePanel; - private ToolTip? _colorNameToolTip; // Put the spectrum images in a bitmap, which is then given to an ImageBrush. private WriteableBitmap? _hueRedBitmap; @@ -117,7 +117,6 @@ namespace Avalonia.Controls.Primitives UnregisterEvents(); // Failsafe - _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); _inputTarget = e.NameScope.Find("PART_InputTarget"); _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); @@ -152,10 +151,10 @@ namespace Avalonia.Controls.Primitives }); } - if (ColorHelpers.ToDisplayNameExists && - _colorNameToolTip != null) + if (_selectionEllipsePanel != null && + ColorHelper.ToDisplayNameExists) { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); + ToolTip.SetTip(_selectionEllipsePanel, ColorHelper.ToDisplayName(Color)); } // If we haven't yet created our bitmaps, do so now. @@ -320,7 +319,7 @@ namespace Avalonia.Controls.Primitives IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small; HsvColor hsvColor = HsvColor; - UpdateColor(ColorHelpers.IncrementColorComponent( + UpdateColor(ColorPickerHelpers.IncrementColorComponent( new Hsv(hsvColor), incrementComponent, direction, @@ -330,34 +329,51 @@ namespace Avalonia.Controls.Primitives maxBound)); e.Handled = true; - - return; } /// protected override void OnGotFocus(GotFocusEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip != null && - ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorHelper.ToDisplayNameExists) { - ToolTip.SetIsOpen(_colorNameToolTip, true); + ToolTip.SetIsOpen(_selectionEllipsePanel, true); } UpdatePseudoClasses(); + + base.OnGotFocus(e); } /// protected override void OnLostFocus(RoutedEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip != null && - ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorHelper.ToDisplayNameExists) { - ToolTip.SetIsOpen(_colorNameToolTip, false); + ToolTip.SetIsOpen(_selectionEllipsePanel, false); } UpdatePseudoClasses(); + + base.OnLostFocus(e); + } + + /// + protected override void OnPointerLeave(PointerEventArgs e) + { + // We only want to bother with the color name tool tip if we can provide color names. + if (_selectionEllipsePanel != null && + ColorHelper.ToDisplayNameExists) + { + ToolTip.SetIsOpen(_selectionEllipsePanel, false); + } + + UpdatePseudoClasses(); + + base.OnPointerLeave(e); } /// @@ -516,12 +532,10 @@ namespace Avalonia.Controls.Primitives var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor); ColorChanged?.Invoke(this, colorChangedEventArgs); - if (ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorHelper.ToDisplayNameExists) { - if (_colorNameToolTip != null) - { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor); - } + ToolTip.SetTip(_selectionEllipsePanel, ColorHelper.ToDisplayName(Color)); } } } @@ -543,7 +557,16 @@ namespace Avalonia.Controls.Primitives PseudoClasses.Set(pcLargeSelector, false); } - PseudoClasses.Set(pcLightSelector, SelectionEllipseShouldBeLight()); + if (SelectionEllipseShouldBeLight()) + { + PseudoClasses.Set(pcDarkSelector, false); + PseudoClasses.Set(pcLightSelector, true); + } + else + { + PseudoClasses.Set(pcDarkSelector, true); + PseudoClasses.Set(pcLightSelector, false); + } } private void UpdateColor(Hsv newHsv) @@ -575,8 +598,10 @@ namespace Avalonia.Controls.Primitives return; } - double xPosition = point.Position.X; - double yPosition = point.Position.Y; + // Remember the bitmap size follows physical device pixels + var scale = LayoutHelper.GetLayoutScale(this); + double xPosition = point.Position.X * scale; + double yPosition = point.Position.Y * scale; double radius = Math.Min(_imageWidthFromLastBitmapCreation, _imageHeightFromLastBitmapCreation) / 2; double distanceFromRadius = Math.Sqrt(Math.Pow(xPosition - radius, 2) + Math.Pow(yPosition - radius, 2)); @@ -807,19 +832,17 @@ namespace Avalonia.Controls.Primitives yPosition = (Math.Sin((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius; } - Canvas.SetLeft(_selectionEllipsePanel, xPosition - (_selectionEllipsePanel.Width / 2)); - Canvas.SetTop(_selectionEllipsePanel, yPosition - (_selectionEllipsePanel.Height / 2)); + // Remember the bitmap size follows physical device pixels + var scale = LayoutHelper.GetLayoutScale(this); + Canvas.SetLeft(_selectionEllipsePanel, (xPosition / scale) - (_selectionEllipsePanel.Width / 2)); + Canvas.SetTop(_selectionEllipsePanel, (yPosition / scale) - (_selectionEllipsePanel.Height / 2)); // We only want to bother with the color name tool tip if we can provide color names. - if (ColorHelpers.ToDisplayNameExists) + if (IsFocused && + _selectionEllipsePanel != null && + ColorHelper.ToDisplayNameExists) { - if (_colorNameToolTip != null) - { - // ToolTip doesn't currently provide any way to re-run its placement logic if its placement target moves, - // so toggling IsEnabled induces it to do that without incurring any visual glitches. - _colorNameToolTip.IsEnabled = false; - _colorNameToolTip.IsEnabled = true; - } + ToolTip.SetIsOpen(_selectionEllipsePanel, true); } UpdatePseudoClasses(); @@ -961,7 +984,14 @@ namespace Avalonia.Controls.Primitives List bgraMaxPixelData = new List(); List newHsvValues = new List(); - var pixelCount = (int)(Math.Round(minDimension) * Math.Round(minDimension)); + // In Avalonia, Bounds returns the actual device-independent pixel size of a control. + // However, this is not necessarily the size of the control rendered on a display. + // A desktop or application scaling factor may be applied which must be accounted for here. + // Remember bitmaps in Avalonia are rendered mapping to actual device pixels, not the device- + // independent pixels of controls. + var scale = LayoutHelper.GetLayoutScale(this); + int pixelDimension = (int)Math.Round(minDimension * scale); + var pixelCount = pixelDimension * pixelDimension; var pixelDataSize = pixelCount * 4; bgraMinPixelData.Capacity = pixelDataSize; @@ -978,8 +1008,6 @@ namespace Avalonia.Controls.Primitives bgraMaxPixelData.Capacity = pixelDataSize; newHsvValues.Capacity = pixelCount; - int minDimensionInt = (int)Math.Round(minDimension); - await Task.Run(() => { // As the user perceives it, every time the third dimension not represented in the ColorSpectrum changes, @@ -998,12 +1026,12 @@ namespace Avalonia.Controls.Primitives // but the running time savings after that are *huge* when we can just set an opacity instead of generating a brand new bitmap. if (shape == ColorSpectrumShape.Box) { - for (int x = minDimensionInt - 1; x >= 0; --x) + for (int x = pixelDimension - 1; x >= 0; --x) { - for (int y = minDimensionInt - 1; y >= 0; --y) + for (int y = pixelDimension - 1; y >= 0; --y) { FillPixelForBox( - x, y, hsv, minDimensionInt, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + x, y, hsv, pixelDimension, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, newHsvValues); } @@ -1011,12 +1039,12 @@ namespace Avalonia.Controls.Primitives } else { - for (int y = 0; y < minDimensionInt; ++y) + for (int y = 0; y < pixelDimension; ++y) { - for (int x = 0; x < minDimensionInt; ++x) + for (int x = 0; x < pixelDimension; ++x) { FillPixelForRing( - x, y, minDimensionInt / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + x, y, pixelDimension / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, newHsvValues); } @@ -1026,13 +1054,13 @@ namespace Avalonia.Controls.Primitives Dispatcher.UIThread.Post(() => { - int pixelWidth = (int)Math.Round(minDimension); - int pixelHeight = (int)Math.Round(minDimension); + int pixelWidth = pixelDimension; + int pixelHeight = pixelDimension; ColorSpectrumComponents components2 = Components; - WriteableBitmap minBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMinPixelData); - WriteableBitmap maxBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMaxPixelData); + WriteableBitmap minBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMinPixelData, pixelWidth, pixelHeight); + WriteableBitmap maxBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMaxPixelData, pixelWidth, pixelHeight); switch (components2) { @@ -1048,18 +1076,18 @@ namespace Avalonia.Controls.Primitives case ColorSpectrumComponents.ValueSaturation: case ColorSpectrumComponents.SaturationValue: _hueRedBitmap = minBitmap; - _hueYellowBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle1PixelData); - _hueGreenBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle2PixelData); - _hueCyanBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle3PixelData); - _hueBlueBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle4PixelData); + _hueYellowBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle1PixelData, pixelWidth, pixelHeight); + _hueGreenBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle2PixelData, pixelWidth, pixelHeight); + _hueCyanBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle3PixelData, pixelWidth, pixelHeight); + _hueBlueBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle4PixelData, pixelWidth, pixelHeight); _huePurpleBitmap = maxBitmap; break; } _shapeFromLastBitmapCreation = Shape; _componentsFromLastBitmapCreation = Components; - _imageWidthFromLastBitmapCreation = minDimension; - _imageHeightFromLastBitmapCreation = minDimension; + _imageWidthFromLastBitmapCreation = pixelDimension; + _imageHeightFromLastBitmapCreation = pixelDimension; _minHueFromLastBitmapCreation = MinHue; _maxHueFromLastBitmapCreation = MaxHue; _minSaturationFromLastBitmapCreation = MinSaturation; @@ -1078,7 +1106,7 @@ namespace Avalonia.Controls.Primitives double x, double y, Hsv baseHsv, - double minDimension, + int minDimension, ColorSpectrumComponents components, double minHue, double maxHue, @@ -1570,7 +1598,7 @@ namespace Avalonia.Controls.Primitives displayedColor = Color; } - var lum = ColorHelpers.GetRelativeLuminance(displayedColor); + var lum = ColorHelper.GetRelativeLuminance(displayedColor); return lum <= 0.5; } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs rename to src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrumShape.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrumShape.cs rename to src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs diff --git a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs new file mode 100644 index 0000000000..4d05222e31 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs @@ -0,0 +1,116 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives.Converters +{ + /// + /// Creates an accent color for a given base color value and step parameter. + /// This is a highly-specialized converter for the color picker. + /// + public class AccentColorConverter : IValueConverter + { + /// + /// The amount to change the Value component for each accent color step. + /// + public const double ValueDelta = 0.1; + + /// + 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; + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + + /// + /// This does not account for perceptual differences and also does not match with + /// system accent color calculation. + /// + /// + /// 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. + /// + /// The base color to calculate the accent from. + /// The number of accent color steps to move. + /// The new accent color. + 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; + } + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToDisplayNameConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToDisplayNameConverter.cs new file mode 100644 index 0000000000..4f727287ba --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToDisplayNameConverter.cs @@ -0,0 +1,68 @@ +using System; +using System.Globalization; +using Avalonia.Controls.Primitives; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Converters +{ + /// + /// Gets the approximated display name for the color. + /// + public class ColorToDisplayNameConverter : IValueConverter + { + /// + 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); + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs new file mode 100644 index 0000000000..9b09073d9d --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -0,0 +1,82 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converts a color to a hex string and vice versa. + /// + public class ColorToHexConverter : IValueConverter + { + /// + 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; + } + + /// + 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; + } + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs new file mode 100644 index 0000000000..220a993f99 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Primitives.Converters +{ + /// + /// Gets the third corresponding with a given + /// that represents the other two components. + /// This is a highly-specialized converter for the color picker. + /// + public class ThirdComponentConverter : IValueConverter + { + /// + 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; + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs new file mode 100644 index 0000000000..9e8c264dd3 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs @@ -0,0 +1,50 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converts the given value into an when a conversion is possible. + /// + public class ToBrushConverter : IValueConverter + { + /// + 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; + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ToColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ToColorConverter.cs new file mode 100644 index 0000000000..14b2be225e --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ToColorConverter.cs @@ -0,0 +1,58 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converts the given value into a when a conversion is possible. + /// + public class ToColorConverter : IValueConverter + { + /// + 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; + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs b/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs new file mode 100644 index 0000000000..2710c220f4 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Primitives.Converters +{ + /// + /// Converter to chain together multiple converters. + /// + public class ValueConverterGroup : List, IValueConverter + { + /// + /// + 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; + } + + /// + 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; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs new file mode 100644 index 0000000000..32a898ee71 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -0,0 +1,142 @@ +using System; +using System.Globalization; +using System.Collections.Generic; +using Avalonia.Media; +using System.Text; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Contains helpers useful when working with colors. + /// + public static class ColorHelper + { + private static readonly Dictionary cachedDisplayNames = new Dictionary(); + private static readonly object cacheMutex = new object(); + + /// + /// Gets the relative (perceptual) luminance/brightness of the given color. + /// 1 is closer to white while 0 is closer to black. + /// + /// The color to calculate relative luminance for. + /// The relative (perceptual) luminance/brightness of the given color. + 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); + } + + /// + /// Determines if color display names are supported based on the current thread culture. + /// + /// + /// Only English names are currently supported following known color names. + /// In the future known color names could be localized. + /// + public static bool ToDisplayNameExists + { + get => CultureInfo.CurrentUICulture.Name.StartsWith("EN", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines an approximate display name for the given color. + /// + /// The color to get the display name for. + /// The approximate color display name. + 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; + } + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs new file mode 100644 index 0000000000..c1904a3c30 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -0,0 +1,629 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Contains internal, special-purpose helpers used with the color picker. + /// + internal static class ColorPickerHelpers + { + /// + /// 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. + /// + /// The pixel width (X, horizontal) of the resulting bitmap. + /// The pixel height (Y, vertical) of the resulting bitmap. + /// The orientation of the resulting bitmap (gradient direction). + /// The color model being used: RGBA or HSVA. + /// The specific color component to sweep. + /// The base HSV color used for components not being changed. + /// Fix the alpha component value to maximum during calculation. + /// This will remove any alpha/transparency from the other component backgrounds. + /// 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. + /// A new bitmap representing a gradient of color component values. + public static async Task 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(() => + { + 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; + } + + /// + /// Converts the given raw BGRA pre-multiplied alpha pixel data into a bitmap. + /// + /// The bitmap (in raw BGRA pre-multiplied alpha pixels). + /// The pixel width of the bitmap. + /// The pixel height of the bitmap. + /// A new . + public static WriteableBitmap CreateBitmapFromPixelData( + IList 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; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs b/src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs b/src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs b/src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs b/src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs diff --git a/src/Avalonia.Controls.ColorPicker/HsvComponent.cs b/src/Avalonia.Controls.ColorPicker/HsvComponent.cs index 1132bd7bbb..1a7a13166a 100644 --- a/src/Avalonia.Controls.ColorPicker/HsvComponent.cs +++ b/src/Avalonia.Controls.ColorPicker/HsvComponent.cs @@ -12,13 +12,21 @@ namespace Avalonia.Controls /// public enum HsvComponent { + /// + /// The Alpha component. + /// + /// + /// Also see: + /// + Alpha = 0, + /// /// The Hue component. /// /// /// Also see: /// - Hue, + Hue = 1, /// /// The Saturation component. @@ -26,7 +34,7 @@ namespace Avalonia.Controls /// /// Also see: /// - Saturation, + Saturation = 2, /// /// The Value component. @@ -34,14 +42,6 @@ namespace Avalonia.Controls /// /// Also see: /// - Value, - - /// - /// The Alpha component. - /// - /// - /// Also see: - /// - Alpha + Value = 3 }; } diff --git a/src/Avalonia.Controls.ColorPicker/RgbComponent.cs b/src/Avalonia.Controls.ColorPicker/RgbComponent.cs new file mode 100644 index 0000000000..c3591573bb --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/RgbComponent.cs @@ -0,0 +1,42 @@ +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Defines a specific component in the RGB color model. + /// + public enum RgbComponent + { + /// + /// The Alpha component. + /// + /// + /// Also see: + /// + Alpha = 0, + + /// + /// The Red component. + /// + /// + /// Also see: + /// + Red = 1, + + /// + /// The Green component. + /// + /// + /// Also see: + /// + Green = 2, + + /// + /// The Blue component. + /// + /// + /// Also see: + /// + Blue = 3 + }; +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml new file mode 100644 index 0000000000..15e5ca1655 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml new file mode 100644 index 0000000000..19f10201a5 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml similarity index 91% rename from src/Avalonia.Controls.ColorPicker/Themes/Default.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 832daf8853..78e6da8aa3 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -1,7 +1,7 @@ - + xmlns:converters="using:Avalonia.Controls.Converters" + x:CompileBindings="True"> @@ -48,7 +48,10 @@ Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + - - - - + VerticalAlignment="Stretch" /> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml new file mode 100644 index 0000000000..cb764a738c --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml new file mode 100644 index 0000000000..18a081721a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml similarity index 89% rename from src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml index 545702ea84..ac8e2a9c06 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -1,7 +1,7 @@ - + xmlns:converters="using:Avalonia.Controls.Converters" + x:CompileBindings="True"> @@ -48,7 +48,10 @@ Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + - - - - + VerticalAlignment="Stretch" /> + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml new file mode 100644 index 0000000000..c25d79727f --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs index b2433bfd97..a91f143019 100644 --- a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs +++ b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; - using Avalonia.Data.Converters; namespace Avalonia.Controls.Converters @@ -22,7 +21,12 @@ namespace Avalonia.Controls.Converters /// public double Scale { get; set; } = 1; - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { if (!(value is CornerRadius radius)) { @@ -36,7 +40,12 @@ namespace Avalonia.Controls.Converters Filter.HasAllFlags(Corners.BottomLeft) ? radius.BottomLeft * Scale : 0); } - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index db1bbdbc6c..0785149a73 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -511,28 +511,39 @@ namespace Avalonia.Controls.Presenters InvalidateMeasure(); } - + + protected override Size MeasureOverride(Size availableSize) { + if (string.IsNullOrEmpty(Text)) + { + return new Size(); + } + _constraint = availableSize; - + _textLayout = null; - + InvalidateArrange(); - var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); - - return new Size(measuredSize.Width, measuredSize.Height); + var measuredSize = TextLayout.Bounds.Size; + + return measuredSize; } protected override Size ArrangeOverride(Size finalSize) { + if (finalSize.Width < TextLayout.Bounds.Width) + { + finalSize = finalSize.WithWidth(TextLayout.Bounds.Width); + } + if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) { return finalSize; } - _constraint = new Size(finalSize.Width, Math.Ceiling(finalSize.Height)); + _constraint = new Size(finalSize.Width, double.PositiveInfinity); _textLayout = null; diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs index 36d2ae9230..8652924f90 100644 --- a/src/Avalonia.Controls/Primitives/IPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; +using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -17,16 +18,50 @@ namespace Avalonia.Controls.Primitives public interface IPopupHost : IDisposable, IFocusScope { /// - /// Sets the control to display in the popup. + /// Gets or sets the fixed width of the popup. /// - /// - void SetChild(IControl? control); + double Width { get; set; } + + /// + /// Gets or sets the minimum width of the popup. + /// + double MinWidth { get; set; } + + /// + /// Gets or sets the maximum width of the popup. + /// + double MaxWidth { get; set; } + + /// + /// Gets or sets the fixed height of the popup. + /// + double Height { get; set; } + + /// + /// Gets or sets the minimum height of the popup. + /// + double MinHeight { get; set; } + + /// + /// Gets or sets the maximum height of the popup. + /// + double MaxHeight { get; set; } /// /// Gets the presenter from the control's template. /// IContentPresenter? Presenter { get; } + /// + /// Gets or sets whether the popup appears on top of all other windows. + /// + bool Topmost { get; set; } + + /// + /// Gets or sets a transform that will be applied to the popup. + /// + Transform? Transform { get; set; } + /// /// Gets the root of the visual tree in the case where the popup is presented using a /// separate visual tree. @@ -57,6 +92,12 @@ namespace Avalonia.Controls.Primitives PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All, Rect? rect = null); + /// + /// Sets the control to display in the popup. + /// + /// + void SetChild(IControl? control); + /// /// Shows the popup. /// @@ -66,14 +107,5 @@ namespace Avalonia.Controls.Primitives /// Hides the popup. /// void Hide(); - - /// - /// Binds the constraints of the popup host to a set of properties, usally those present on - /// . - /// - IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, - StyledProperty minWidthProperty, StyledProperty maxWidthProperty, - StyledProperty heightProperty, StyledProperty minHeightProperty, - StyledProperty maxHeightProperty, StyledProperty topmostProperty); } } diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 6ac544e0fe..4765718c3b 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -11,6 +11,12 @@ namespace Avalonia.Controls.Primitives { public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup { + /// + /// Defines the property. + /// + public static readonly StyledProperty TransformProperty = + PopupRoot.TransformProperty.AddOwner(); + private readonly OverlayLayer _overlayLayer; private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters(); private ManagedPopupPositioner _positioner; @@ -29,10 +35,22 @@ namespace Avalonia.Controls.Primitives } public IVisual? HostedVisualTreeRoot => null; - + + public Transform? Transform + { + get => GetValue(TransformProperty); + set => SetValue(TransformProperty, value); + } + /// IInteractive? IInteractive.InteractiveParent => Parent; + bool IPopupHost.Topmost + { + get => false; + set { /* Not currently supported in overlay popups */ } + } + public void Dispose() => Hide(); @@ -48,28 +66,6 @@ namespace Avalonia.Controls.Primitives _shown = false; } - public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, - StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, - StyledProperty maxHeightProperty, StyledProperty topmostProperty) - { - // Topmost property is not supported - var bindings = new List(); - - void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); - Bind(WidthProperty, widthProperty); - Bind(MinWidthProperty, minWidthProperty); - Bind(MaxWidthProperty, maxWidthProperty); - Bind(HeightProperty, heightProperty); - Bind(MinHeightProperty, minHeightProperty); - Bind(MaxHeightProperty, maxHeightProperty); - - return Disposable.Create(() => - { - foreach (var x in bindings) - x.Dispose(); - }); - } - public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None, PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All, diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index bb546107e0..7a7e41b029 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -14,6 +14,8 @@ using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.VisualTree; +using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { @@ -33,6 +35,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty ChildProperty = AvaloniaProperty.Register(nameof(Child)); + /// + /// Defines the property. + /// + public static readonly StyledProperty InheritsTransformProperty = + AvaloniaProperty.Register(nameof(InheritsTransform)); + /// /// Defines the property. /// @@ -196,6 +204,16 @@ namespace Avalonia.Controls.Primitives set; } + /// + /// Gets or sets a value that determines whether the popup inherits the render transform + /// from its . Defaults to false. + /// + public bool InheritsTransform + { + get => GetValue(InheritsTransformProperty); + set => SetValue(InheritsTransformProperty, value); + } + /// /// Gets or sets a value that determines how the can be dismissed. /// @@ -395,24 +413,29 @@ namespace Avalonia.Controls.Primitives } _isOpenRequested = false; - var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); + var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); var handlerCleanup = new CompositeDisposable(7); - popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, - HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup); - + UpdateHostSizing(popupHost, topLevel, placementTarget); + popupHost.Topmost = Topmost; popupHost.SetChild(Child); ((ISetLogicalParent)popupHost).SetParent(this); - popupHost.ConfigurePosition( - placementTarget, - PlacementMode, - new Point(HorizontalOffset, VerticalOffset), - PlacementAnchor, - PlacementGravity, - PlacementConstraintAdjustment, - PlacementRect); + if (InheritsTransform && placementTarget is Control c) + { + SubscribeToEventHandler>( + c, + PlacementTargetPropertyChanged, + (x, handler) => x.PropertyChanged += handler, + (x, handler) => x.PropertyChanged -= handler).DisposeWith(handlerCleanup); + } + else + { + popupHost.Transform = null; + } + + UpdateHostPosition(popupHost, placementTarget); SubscribeToEventHandler>(popupHost, RootTemplateApplied, (x, handler) => x.TemplateApplied += handler, @@ -494,7 +517,7 @@ namespace Avalonia.Controls.Primitives } } - _openState = new PopupOpenState(topLevel, popupHost, cleanupPopup); + _openState = new PopupOpenState(placementTarget, topLevel, popupHost, cleanupPopup); WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint); @@ -542,7 +565,93 @@ namespace Avalonia.Controls.Primitives base.OnDetachedFromLogicalTree(e); Close(); } - + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (_openState is not null) + { + if (change.Property == WidthProperty || + change.Property == MinWidthProperty || + change.Property == MaxWidthProperty || + change.Property == HeightProperty || + change.Property == MinHeightProperty || + change.Property == MaxHeightProperty) + { + UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget); + } + else if (change.Property == PlacementTargetProperty || + change.Property == PlacementModeProperty || + change.Property == HorizontalOffsetProperty || + change.Property == VerticalOffsetProperty || + change.Property == PlacementAnchorProperty || + change.Property == PlacementConstraintAdjustmentProperty || + change.Property == PlacementRectProperty) + { + if (change.Property == PlacementTargetProperty) + { + var newTarget = change.GetNewValue() ?? this.FindLogicalAncestorOfType(); + + if (newTarget is null || newTarget.GetVisualRoot() != _openState.TopLevel) + { + Close(); + return; + } + + _openState.PlacementTarget = newTarget; + } + + UpdateHostPosition(_openState.PopupHost, _openState.PlacementTarget); + } + else if (change.Property == TopmostProperty) + { + _openState.PopupHost.Topmost = change.GetNewValue(); + } + } + } + + private void UpdateHostPosition(IPopupHost popupHost, IControl placementTarget) + { + popupHost.ConfigurePosition( + placementTarget, + PlacementMode, + new Point(HorizontalOffset, VerticalOffset), + PlacementAnchor, + PlacementGravity, + PlacementConstraintAdjustment, + PlacementRect ?? new Rect(default, placementTarget.Bounds.Size)); + } + + private void UpdateHostSizing(IPopupHost popupHost, TopLevel topLevel, IControl placementTarget) + { + var scaleX = 1.0; + var scaleY = 1.0; + + if (InheritsTransform && placementTarget.TransformToVisual(topLevel) is Matrix m) + { + scaleX = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12); + scaleY = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12); + + // Ideally we'd only assign a ScaleTransform here when the scale != 1, but there's + // an issue with LayoutTransformControl in that it sets its LayoutTransform property + // with LocalValue priority in ArrangeOverride in certain cases when LayoutTransform + // is null, which breaks TemplateBindings to this property. Offending commit/line: + // + // https://github.com/AvaloniaUI/Avalonia/commit/6fbe1c2180ef45a940e193f1b4637e64eaab80ed#diff-5344e793df13f462126a8153ef46c44194f244b6890f25501709bae51df97f82R54 + popupHost.Transform = new ScaleTransform(scaleX, scaleY); + } + else + { + popupHost.Transform = null; + } + + popupHost.Width = Width * scaleX; + popupHost.MinWidth = MinWidth * scaleX; + popupHost.MaxWidth = MaxWidth * scaleX; + popupHost.Height = Height * scaleY; + popupHost.MinHeight = MinHeight * scaleY; + popupHost.MaxHeight = MaxHeight * scaleY; + } + private void HandlePositionChange() { if (_openState != null) @@ -824,6 +933,14 @@ namespace Avalonia.Controls.Primitives } } + private void PlacementTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (_openState is not null && e.Property == Visual.TransformedBoundsProperty) + { + UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget); + } + } + private void WindowLostFocus() { if (IsLightDismissEnabled) @@ -862,15 +979,16 @@ namespace Avalonia.Controls.Primitives private readonly IDisposable _cleanup; private IDisposable? _presenterCleanup; - public PopupOpenState(TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup) + public PopupOpenState(IControl placementTarget, TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup) { + PlacementTarget = placementTarget; TopLevel = topLevel; PopupHost = popupHost; _cleanup = cleanup; } public TopLevel TopLevel { get; } - + public IControl PlacementTarget { get; set; } public IPopupHost PopupHost { get; } public void SetPresenterSubscription(IDisposable? presenterCleanup) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index abf56e5420..f7bf7c1a27 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Reactive.Disposables; using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; @@ -8,7 +6,6 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.VisualTree; -using JetBrains.Annotations; namespace Avalonia.Controls.Primitives { @@ -17,6 +14,12 @@ namespace Avalonia.Controls.Primitives /// public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { + /// + /// Defines the property. + /// + public static readonly StyledProperty TransformProperty = + AvaloniaProperty.Register(nameof(Transform)); + private PopupPositionerParameters _positionerParameters; /// @@ -54,6 +57,15 @@ namespace Avalonia.Controls.Primitives /// public new IPopupImpl? PlatformImpl => (IPopupImpl?)base.PlatformImpl; + /// + /// Gets or sets a transform that will be applied to the popup. + /// + public Transform? Transform + { + get => GetValue(TransformProperty); + set => SetValue(TransformProperty, value); + } + /// /// Gets the parent control in the event route. /// @@ -103,27 +115,6 @@ namespace Avalonia.Controls.Primitives IVisual IPopupHost.HostedVisualTreeRoot => this; - public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, - StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, - StyledProperty maxHeightProperty, StyledProperty topmostProperty) - { - var bindings = new List(); - - void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); - Bind(WidthProperty, widthProperty); - Bind(MinWidthProperty, minWidthProperty); - Bind(MaxWidthProperty, maxWidthProperty); - Bind(HeightProperty, heightProperty); - Bind(MinHeightProperty, minHeightProperty); - Bind(MaxHeightProperty, maxHeightProperty); - Bind(TopmostProperty, topmostProperty); - return Disposable.Create(() => - { - foreach (var x in bindings) - x.Dispose(); - }); - } - protected override Size MeasureOverride(Size availableSize) { var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c04a62008b..bbe6aeb7ee 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -604,7 +604,9 @@ namespace Avalonia.Controls return new Size(); } - var padding = Padding; + var scale = LayoutHelper.GetLayoutScale(this); + + var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); _constraint = availableSize.Deflate(padding); @@ -612,23 +614,24 @@ namespace Avalonia.Controls InvalidateArrange(); - var measuredSize = PixelSize.FromSize(TextLayout.Bounds.Size, 1); + var measuredSize = TextLayout.Bounds.Size.Inflate(padding); - return new Size(measuredSize.Width, measuredSize.Height).Inflate(padding); + return measuredSize; } protected override Size ArrangeOverride(Size finalSize) { + if(finalSize.Width < TextLayout.Bounds.Width) + { + finalSize = finalSize.WithWidth(TextLayout.Bounds.Width); + } + if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) { return finalSize; } - var padding = Padding; - - var textSize = finalSize.Deflate(padding); - - _constraint = new Size(textSize.Width, Math.Ceiling(textSize.Height)); + _constraint = new Size(finalSize.Width, double.PositiveInfinity); _textLayout = null; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs index aa03330cc5..7384daae30 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -1,13 +1,17 @@ +using System; +using Avalonia.Data; +using Avalonia.Media; + namespace Avalonia.Diagnostics.ViewModels { internal class AvaloniaPropertyViewModel : PropertyViewModel { private readonly AvaloniaObject _target; - private System.Type _assignedType; + private Type _assignedType; private object? _value; private string _priority; private string _group; - private readonly System.Type _propertyType; + private readonly Type _propertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -28,13 +32,9 @@ namespace Avalonia.Diagnostics.ViewModels public AvaloniaProperty Property { get; } public override object Key => Property; public override string Name { get; } - public override bool? IsAttached => - Property.IsAttached; - - public override string Priority => - _priority; - - public override System.Type AssignedType => _assignedType; + public override bool? IsAttached => Property.IsAttached; + public override string Priority => _priority; + public override Type AssignedType => _assignedType; public override string? Value { @@ -53,30 +53,58 @@ namespace Avalonia.Diagnostics.ViewModels public override string Group => _group; - public override System.Type? DeclaringType { get; } - public override System.Type PropertyType => _propertyType; + public override Type? DeclaringType { get; } + public override Type PropertyType => _propertyType; // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))] public override void Update() { if (Property.IsDirect) { - RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType,_value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); + object? value; + Type? valueType = null; + + try + { + value = _target.GetValue(Property); + valueType = value?.GetType(); + } + catch (Exception e) + { + value = e.GetBaseException(); + } + + RaiseAndSetIfChanged(ref _value, value, nameof(Value)); + RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority)); _group = "Properties"; } else { - var val = _target.GetDiagnostic(Property); + object? value; + Type? valueType = null; + BindingPriority? priority = null; + + try + { + var diag = _target.GetDiagnostic(Property); + + value = diag.Value; + valueType = value?.GetType(); + priority = diag.Priority; + } + catch (Exception e) + { + value = e.GetBaseException(); + } - RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); + RaiseAndSetIfChanged(ref _value, value, nameof(Value)); + RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); - if (val != null) + if (priority != null) { - RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority)); + RaiseAndSetIfChanged(ref _priority, priority.ToString()!, nameof(Priority)); RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group)); } else diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs index e2d8a30c8a..73fb615b32 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -1,13 +1,15 @@ -using System.Reflection; +using System; +using System.Reflection; +using Avalonia.Media; namespace Avalonia.Diagnostics.ViewModels { internal class ClrPropertyViewModel : PropertyViewModel { private readonly object _target; - private System.Type _assignedType; + private Type _assignedType; private object? _value; - private readonly System.Type _propertyType; + private readonly Type _propertyType; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -25,6 +27,7 @@ namespace Avalonia.Diagnostics.ViewModels { Name = property.DeclaringType.Name + '.' + property.Name; } + DeclaringType = property.DeclaringType; _propertyType = property.PropertyType; @@ -36,10 +39,10 @@ namespace Avalonia.Diagnostics.ViewModels public override string Name { get; } public override string Group => "CLR Properties"; - public override System.Type AssignedType => _assignedType; - public override System.Type PropertyType => _propertyType; + public override Type AssignedType => _assignedType; + public override Type PropertyType => _propertyType; - public override string? Value + public override string? Value { get => ConvertToString(_value); set @@ -54,20 +57,30 @@ namespace Avalonia.Diagnostics.ViewModels } } - public override string Priority => - string.Empty; + public override string Priority => string.Empty; - public override bool? IsAttached => - default; + public override bool? IsAttached => default; - public override System.Type? DeclaringType { get; } + public override Type? DeclaringType { get; } // [MemberNotNull(nameof(_type))] public override void Update() { - var val = Property.GetValue(_target); - RaiseAndSetIfChanged(ref _value, val, nameof(Value)); - RaiseAndSetIfChanged(ref _assignedType, _value?.GetType() ?? Property.PropertyType, nameof(AssignedType)); + object? value; + Type? valueType = null; + + try + { + value = Property.GetValue(_target); + valueType = value?.GetType(); + } + catch (Exception e) + { + value = e.GetBaseException(); + } + + RaiseAndSetIfChanged(ref _value, value, nameof(Value)); + RaiseAndSetIfChanged(ref _assignedType, valueType ?? Property.PropertyType, nameof(AssignedType)); RaisePropertyChanged(nameof(Type)); } } diff --git a/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml index a3a4cf4662..d7bf4bbbf1 100644 --- a/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml @@ -1,9 +1,13 @@ - + diff --git a/src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml index c5d7ad55d7..81bd8f39c5 100644 --- a/src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml @@ -4,6 +4,7 @@ Selector="NativeMenuBar"> + @@ -18,6 +19,8 @@ + + diff --git a/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml b/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml index 301e19d208..07d905ea1d 100644 --- a/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml +++ b/src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml @@ -1,4 +1,4 @@ - diff --git a/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml b/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml index 9468cc5535..5e8f3337ee 100644 --- a/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml +++ b/src/Avalonia.Themes.Default/Controls/PopupRoot.xaml @@ -10,16 +10,18 @@ - - - - - - + + + + + + + + diff --git a/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs b/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs new file mode 100644 index 0000000000..9b7fcecf45 --- /dev/null +++ b/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Media.Imaging; + +namespace Avalonia.Themes.Default +{ + internal class IBitmapToImageConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value != null && value is IBitmap bm) + return new Image { Source=bm }; + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index e35093d2f1..93ecc438eb 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -119,7 +119,8 @@ MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MaxHeight="{TemplateBinding MaxDropDownHeight}" PlacementTarget="Background" - IsLightDismissEnabled="True"> + IsLightDismissEnabled="True" + InheritsTransform="True"> - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml b/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml index 5d9d04d218..31b43c39cd 100644 --- a/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml @@ -6,13 +6,15 @@ - - - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml index 1e573913b9..f608cf55f5 100644 --- a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml @@ -10,16 +10,18 @@ - - - - + + + + - - + + + diff --git a/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs b/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs new file mode 100644 index 0000000000..34670882f8 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Media.Imaging; + +namespace Avalonia.Themes.Fluent +{ + internal class IBitmapToImageConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value != null && value is IBitmap bm) + return new Image { Source=bm }; + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index db33b88cc3..add97a660b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -39,17 +39,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions targetType = setter.Property.PropertyType; } - // Look upwards though the ambient context for IResourceHosts and IResourceProviders + // Look upwards though the ambient context for IResourceNodes // which might be able to give us the resource. - foreach (var e in stack.Parents) + foreach (var parent in stack.Parents) { - object value; - - if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value)) - { - return ColorToBrushConverter.Convert(value, targetType); - } - else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value)) + if (parent is IResourceNode node && node.TryGetResource(ResourceKey, out var value)) { return ColorToBrushConverter.Convert(value, targetType); } @@ -58,7 +52,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (provideTarget.TargetObject is IControl target && provideTarget.TargetProperty is PropertyInfo property) { - DelayedBinding.Add(target, property, x => GetValue(x, targetType)); + // This is stored locally to avoid allocating closure in the outer scope. + var localTargetType = targetType; + var localInstance = this; + + DelayedBinding.Add(target, property, x => localInstance.GetValue(x, localTargetType)); return AvaloniaProperty.UnsetValue; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 607b552c28..fa4a27fc50 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -60,7 +60,7 @@ namespace Avalonia.Markup.Xaml.Styling } } - bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; + bool IResourceNode.HasResources => Loaded?.HasResources ?? false; IReadOnlyList IStyle.Children => _loaded ?? Array.Empty(); @@ -86,9 +86,9 @@ namespace Avalonia.Markup.Xaml.Styling public bool TryGetResource(object key, out object? value) { - if (!_isLoading && Loaded is IResourceProvider p) + if (!_isLoading) { - return p.TryGetResource(key, out value); + return Loaded.TryGetResource(key, out value); } value = null; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2548b9f5aa..b45968ec27 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -126,6 +126,7 @@ namespace Avalonia.Skia SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas; SKSurface ISkiaDrawingContextImpl.SkSurface => Surface; GRContext ISkiaDrawingContextImpl.GrContext => _grContext; + double ISkiaDrawingContextImpl.CurrentOpacity => _currentOpacity; /// public void Clear(Color color) diff --git a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs index 38fa5a5253..87c89beb7e 100644 --- a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs @@ -8,5 +8,6 @@ namespace Avalonia.Skia SKCanvas SkCanvas { get; } GRContext GrContext { get; } SKSurface SkSurface { get; } + double CurrentOpacity { get; } } } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index ebaa247da8..908b0ffa47 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -27,7 +27,7 @@ namespace Avalonia.Skia buffer.GuessSegmentProperties(); - buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; + buffer.Direction = Direction.LeftToRight; //Always shape LeftToRight buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); @@ -35,11 +35,6 @@ namespace Avalonia.Skia font.Shape(buffer); - if (buffer.Direction == Direction.RightToLeft) - { - buffer.Reverse(); - } - font.GetScale(out var scaleX, out _); var textScale = fontRenderingEmSize / scaleX; diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 59027a663f..f4e4b00147 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -11,8 +10,7 @@ using GlyphInfo = HarfBuzzSharp.GlyphInfo; namespace Avalonia.Direct2D1.Media { - -internal class TextShaperImpl : ITextShaperImpl + internal class TextShaperImpl : ITextShaperImpl { public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) { @@ -23,25 +21,20 @@ internal class TextShaperImpl : ITextShaperImpl using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); + buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length); MergeBreakPair(buffer); - + buffer.GuessSegmentProperties(); - buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; + buffer.Direction = Direction.LeftToRight; //Always shape LeftToRight - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); var font = ((GlyphTypefaceImpl)typeface.PlatformImpl).Font; font.Shape(buffer); - if (buffer.Direction == Direction.RightToLeft) - { - buffer.Reverse(); - } - font.GetScale(out var scaleX, out _); var textScale = fontRenderingEmSize / scaleX; @@ -60,13 +53,13 @@ internal class TextShaperImpl : ITextShaperImpl var glyphIndex = (ushort)sourceInfo.Codepoint; - var glyphCluster = (int)sourceInfo.Cluster; + var glyphCluster = (int)(sourceInfo.Cluster); var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (glyphIndex == 0 && text[glyphCluster] == '\t') + if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t') { glyphIndex = typeface.GetGlyph(' '); @@ -75,9 +68,7 @@ internal class TextShaperImpl : ITextShaperImpl 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - var targetInfo = - new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, - glyphOffset); + var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); shapedBuffer[i] = targetInfo; } @@ -91,7 +82,7 @@ internal class TextShaperImpl : ITextShaperImpl var length = buffer.Length; var glyphInfos = buffer.GetGlyphInfoSpan(); - + var second = glyphInfos[length - 1]; if (!new Codepoint((int)second.Codepoint).IsBreakChar) @@ -102,7 +93,7 @@ internal class TextShaperImpl : ITextShaperImpl if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') { var first = glyphInfos[length - 2]; - + first.Codepoint = '\u200C'; second.Codepoint = '\u200C'; second.Cluster = first.Cluster; @@ -113,7 +104,7 @@ internal class TextShaperImpl : ITextShaperImpl { *p = first; } - + fixed (GlyphInfo* p = &glyphInfos[length - 1]) { *p = second; @@ -148,7 +139,7 @@ internal class TextShaperImpl : ITextShaperImpl private static double GetGlyphAdvance(ReadOnlySpan glyphPositions, int index, double textScale) { // Depends on direction of layout - // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale; + // glyphPositions[index].YAdvance * textScale; return glyphPositions[index].XAdvance * textScale; } } diff --git a/src/iOS/Avalonia.iOS/TouchHandler.cs b/src/iOS/Avalonia.iOS/TouchHandler.cs index 43b19c85af..959a660d8a 100644 --- a/src/iOS/Avalonia.iOS/TouchHandler.cs +++ b/src/iOS/Avalonia.iOS/TouchHandler.cs @@ -41,7 +41,7 @@ namespace Avalonia.iOS _ => RawPointerEventType.TouchUpdate }, pt, RawInputModifiers.None, id); - _device.ProcessRawEvent(ev); + _tl.Input?.Invoke(ev); if (t.Phase == UITouchPhase.Cancelled || t.Phase == UITouchPhase.Ended) _knownTouches.Remove(t); @@ -49,4 +49,4 @@ namespace Avalonia.iOS } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index f27ff3928c..6d3351d2b2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -78,9 +78,13 @@ namespace Avalonia.Controls.UnitTests.Primitives var templatedChild = ((Visual)target.Host).GetVisualChildren().Single(); - Assert.IsType(templatedChild); + Assert.IsType(templatedChild); - var visualLayerManager = templatedChild.GetVisualChildren().Skip(1).Single(); + var panel = templatedChild.GetVisualChildren().Single(); + + Assert.IsType(panel); + + var visualLayerManager = panel.GetVisualChildren().Skip(1).Single(); Assert.IsType(visualLayerManager); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index cac8ca885d..d9cb40d1cc 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -325,6 +325,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new[] { + "LayoutTransformControl", "VisualLayerManager", "ContentPresenter", "ContentPresenter", @@ -337,6 +338,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new[] { + "LayoutTransformControl", "Panel", "Border", "VisualLayerManager", @@ -356,6 +358,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal( new object[] { + popupRoot, popupRoot, popupRoot, target, @@ -372,6 +375,7 @@ namespace Avalonia.Controls.UnitTests.Primitives popupRoot, popupRoot, popupRoot, + popupRoot, target, null, }, diff --git a/tests/Avalonia.UnitTests/TestLogSink.cs b/tests/Avalonia.UnitTests/TestLogSink.cs index e10292a59b..8b8084ca17 100644 --- a/tests/Avalonia.UnitTests/TestLogSink.cs +++ b/tests/Avalonia.UnitTests/TestLogSink.cs @@ -37,23 +37,6 @@ namespace Avalonia.UnitTests _callback(level, area, source, messageTemplate); } - public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0) - { - _callback(level, area, source, messageTemplate, propertyValue0); - } - - public void Log(LogEventLevel level, string area, object source, string messageTemplate, - T0 propertyValue0, T1 propertyValue1) - { - _callback(level, area, source, messageTemplate, propertyValue0, propertyValue1); - } - - public void Log(LogEventLevel level, string area, object source, string messageTemplate, - T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - _callback(level, area, source, messageTemplate, propertyValue0, propertyValue1, propertyValue2); - } - public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues) { diff --git a/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png b/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png index edd4dfd263..e8624fa457 100644 Binary files a/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png and b/tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png differ diff --git a/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png b/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png index a76c6a5b2a..7bd622050e 100644 Binary files a/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png and b/tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png differ