#import #import static NSString *const kElectrobunVibrancyViewIdentifier = @"ElectrobunVibrancyView"; static NSString *const kElectrobunNativeDragViewIdentifier = @"ElectrobunNativeDragView"; static NSString *const kElectrobunWindowBorderViewIdentifier = @"ElectrobunWindowBorderView"; static CGFloat gWindowCornerRadius = 15.0; static CGFloat gTrafficLightsX = 16.0; static CGFloat gTrafficLightsY = 12.0; static const void *kElectrobunChromeObserverKey = &kElectrobunChromeObserverKey; @interface ElectrobunWindowBorderView : NSView @property(nonatomic) CGFloat cornerRadius; @end @implementation ElectrobunWindowBorderView @synthesize cornerRadius = _cornerRadius; - (instancetype)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; if (self) { _cornerRadius = gWindowCornerRadius; } return self; } - (BOOL)isOpaque { return NO; } - (BOOL)acceptsFirstMouse:(NSEvent *)event { (void)event; return NO; } - (NSView *)hitTest:(NSPoint)point { (void)point; return nil; } - (void)setFrame:(NSRect)frame { [super setFrame:frame]; [self setNeedsDisplay:YES]; } - (void)drawRect:(NSRect)dirtyRect { (void)dirtyRect; NSRect bounds = [self bounds]; CGFloat radius = MAX(0.0, self.cornerRadius); NSBezierPath *borderPath = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(bounds, 0.5, 0.5) xRadius:radius yRadius:radius]; if (@available(macOS 10.14, *)) { NSAppearance *appearance = [self effectiveAppearance]; BOOL isDark = [[appearance bestMatchFromAppearancesWithNames:@[ NSAppearanceNameAqua, NSAppearanceNameDarkAqua ]] isEqualToString:NSAppearanceNameDarkAqua]; if (isDark) { [[NSColor colorWithWhite:1.0 alpha:0.18] setStroke]; } else { [[NSColor colorWithWhite:1.0 alpha:0.72] setStroke]; } } else { [[NSColor colorWithWhite:1.0 alpha:0.72] setStroke]; } borderPath.lineWidth = 1.0; [borderPath stroke]; } @end @interface ElectrobunNativeDragView : NSView @end @implementation ElectrobunNativeDragView - (BOOL)isOpaque { return NO; } - (void)drawRect:(NSRect)dirtyRect { (void)dirtyRect; } - (void)mouseDown:(NSEvent *)event { NSWindow *window = [self window]; if (window != nil && event != nil) { [window performWindowDragWithEvent:event]; } } @end @interface ElectrobunWindowChromeObserver : NSObject @property(nonatomic, weak) NSWindow *window; @end static bool applyTrafficLightsPosition(NSWindow *window, CGFloat x, CGFloat yFromTop); static void refreshWindowChrome(NSWindow *window); static BOOL isWindowFullScreen(NSWindow *window); @implementation ElectrobunWindowChromeObserver - (instancetype)initWithWindow:(NSWindow *)window { self = [super init]; if (self) { _window = window; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(refreshChrome) name:NSWindowDidResizeNotification object:window]; [center addObserver:self selector:@selector(refreshChrome) name:NSWindowDidEndLiveResizeNotification object:window]; [center addObserver:self selector:@selector(refreshChrome) name:NSWindowDidEnterFullScreenNotification object:window]; [center addObserver:self selector:@selector(refreshChrome) name:NSWindowDidExitFullScreenNotification object:window]; [center addObserver:self selector:@selector(refreshChrome) name:NSWindowDidBecomeKeyNotification object:window]; [center addObserver:self selector:@selector(refreshChrome) name:NSWindowWillEnterFullScreenNotification object:window]; } return self; } - (void)refreshChrome { if (self.window != nil) { refreshWindowChrome(self.window); applyTrafficLightsPosition(self.window, gTrafficLightsX, gTrafficLightsY); } } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end static NSVisualEffectView *findVibrancyView(NSView *contentView) { for (NSView *subview in [contentView subviews]) { if ([subview isKindOfClass:[NSVisualEffectView class]] && [[subview identifier] isEqualToString:kElectrobunVibrancyViewIdentifier]) { return (NSVisualEffectView *)subview; } } return nil; } static ElectrobunNativeDragView *findNativeDragView(NSView *contentView) { for (NSView *subview in [contentView subviews]) { if ([subview isKindOfClass:[ElectrobunNativeDragView class]] && [[subview identifier] isEqualToString:kElectrobunNativeDragViewIdentifier]) { return (ElectrobunNativeDragView *)subview; } } return nil; } static ElectrobunWindowBorderView *findWindowBorderView(NSView *contentView) { for (NSView *subview in [contentView subviews]) { if ([subview isKindOfClass:[ElectrobunWindowBorderView class]] && [[subview identifier] isEqualToString:kElectrobunWindowBorderViewIdentifier]) { return (ElectrobunWindowBorderView *)subview; } } return nil; } static BOOL isWindowFullScreen(NSWindow *window) { return (window.styleMask & NSWindowStyleMaskFullScreen) != 0; } static void refreshWindowChrome(NSWindow *window) { BOOL fullScreen = isWindowFullScreen(window); CGFloat radius = fullScreen ? 0.0 : gWindowCornerRadius; NSView *contentView = [window contentView]; if (contentView == nil) { return; } contentView.wantsLayer = YES; contentView.layer.cornerRadius = radius; contentView.layer.masksToBounds = YES; NSVisualEffectView *effectView = findVibrancyView(contentView); if (effectView != nil) { effectView.wantsLayer = YES; effectView.layer.cornerRadius = radius; effectView.layer.masksToBounds = YES; } ElectrobunWindowBorderView *borderView = findWindowBorderView(contentView); if (borderView != nil) { borderView.hidden = fullScreen; if (!fullScreen) { borderView.cornerRadius = gWindowCornerRadius; [borderView setNeedsDisplay:YES]; } } } static void applyWindowCornerRadius(NSWindow *window, CGFloat radius) { gWindowCornerRadius = MAX(0.0, radius); refreshWindowChrome(window); } static void ensureWindowBorder(NSWindow *window) { NSView *contentView = [window contentView]; if (contentView == nil) { return; } ElectrobunWindowBorderView *borderView = findWindowBorderView(contentView); if (borderView == nil) { borderView = [[ElectrobunWindowBorderView alloc] initWithFrame:[contentView bounds]]; [borderView setIdentifier:kElectrobunWindowBorderViewIdentifier]; [borderView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; } [borderView setFrame:[contentView bounds]]; if ([borderView superview] == nil) { [contentView addSubview:borderView positioned:NSWindowAbove relativeTo:nil]; } else { [borderView removeFromSuperview]; [contentView addSubview:borderView positioned:NSWindowAbove relativeTo:nil]; } refreshWindowChrome(window); } static bool applyTrafficLightsPosition(NSWindow *window, CGFloat x, CGFloat yFromTop) { NSButton *closeButton = [window standardWindowButton:NSWindowCloseButton]; NSButton *minimizeButton = [window standardWindowButton:NSWindowMiniaturizeButton]; NSButton *zoomButton = [window standardWindowButton:NSWindowZoomButton]; if (closeButton == nil || minimizeButton == nil || zoomButton == nil) { return false; } NSView *buttonContainer = [closeButton superview]; if (buttonContainer == nil) { return false; } CGFloat spacing = NSMinX(minimizeButton.frame) - NSMinX(closeButton.frame); if (spacing <= 0) { spacing = closeButton.frame.size.width + 6.0; } BOOL flipped = [buttonContainer isFlipped]; CGFloat targetY = yFromTop; if (!flipped) { targetY = buttonContainer.frame.size.height - yFromTop - closeButton.frame.size.height; } targetY = MAX(0.0, targetY); CGFloat currentX = x; NSArray *buttons = @[ closeButton, minimizeButton, zoomButton ]; for (NSButton *button in buttons) { [button setAutoresizingMask:NSViewNotSizable]; [button setFrameOrigin:NSMakePoint(currentX, targetY)]; currentX += spacing; } return true; } static void ensureTrafficLightsObserver(NSWindow *window) { ElectrobunWindowChromeObserver *existingObserver = objc_getAssociatedObject(window, kElectrobunChromeObserverKey); if (existingObserver != nil) { return; } ElectrobunWindowChromeObserver *observer = [[ElectrobunWindowChromeObserver alloc] initWithWindow:window]; objc_setAssociatedObject(window, kElectrobunChromeObserverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } static void scheduleTrafficLightsPosition(NSWindow *window, NSInteger attempt) { if (applyTrafficLightsPosition(window, gTrafficLightsX, gTrafficLightsY)) { return; } if (attempt >= 10) { return; } dispatch_after( dispatch_time(DISPATCH_TIME_NOW, (int64_t)(50 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ scheduleTrafficLightsPosition(window, attempt + 1); }); } extern "C" bool enableWindowVibrancy(void *windowPtr, double cornerRadius) { if (windowPtr == nullptr) { return false; } __block BOOL success = NO; dispatch_sync(dispatch_get_main_queue(), ^{ NSWindow *window = (__bridge NSWindow *)windowPtr; if (![window isKindOfClass:[NSWindow class]]) { return; } applyWindowCornerRadius(window, (CGFloat)cornerRadius); [window setOpaque:NO]; [window setBackgroundColor:[NSColor clearColor]]; [window setTitlebarAppearsTransparent:YES]; [window setHasShadow:YES]; NSView *contentView = [window contentView]; if (contentView == nil) { return; } NSVisualEffectView *effectView = findVibrancyView(contentView); if (effectView == nil) { effectView = [[NSVisualEffectView alloc] initWithFrame:[contentView bounds]]; [effectView setIdentifier:kElectrobunVibrancyViewIdentifier]; [effectView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; } if (@available(macOS 10.14, *)) { [effectView setMaterial:NSVisualEffectMaterialHUDWindow]; } else { [effectView setMaterial:NSVisualEffectMaterialSidebar]; } [effectView setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; [effectView setState:NSVisualEffectStateActive]; if ([effectView superview] == nil) { NSView *relativeView = [[contentView subviews] firstObject]; if (relativeView != nil) { [contentView addSubview:effectView positioned:NSWindowBelow relativeTo:relativeView]; } else { [contentView addSubview:effectView]; } } ensureWindowBorder(window); applyWindowCornerRadius(window, gWindowCornerRadius); ensureTrafficLightsObserver(window); scheduleTrafficLightsPosition(window, 0); [window invalidateShadow]; success = YES; }); return success; } extern "C" bool setWindowCornerRadius(void *windowPtr, double cornerRadius) { if (windowPtr == nullptr) { return false; } __block BOOL success = NO; dispatch_sync(dispatch_get_main_queue(), ^{ NSWindow *window = (__bridge NSWindow *)windowPtr; if (![window isKindOfClass:[NSWindow class]]) { return; } applyWindowCornerRadius(window, (CGFloat)cornerRadius); ensureWindowBorder(window); [window invalidateShadow]; success = YES; }); return success; } extern "C" bool ensureWindowShadow(void *windowPtr) { if (windowPtr == nullptr) { return false; } __block BOOL success = NO; dispatch_sync(dispatch_get_main_queue(), ^{ NSWindow *window = (__bridge NSWindow *)windowPtr; if (![window isKindOfClass:[NSWindow class]]) { return; } [window setHasShadow:YES]; [window invalidateShadow]; ensureWindowBorder(window); applyWindowCornerRadius(window, gWindowCornerRadius); success = YES; }); return success; } extern "C" bool setWindowTrafficLightsPosition(void *windowPtr, double x, double yFromTop) { if (windowPtr == nullptr) { return false; } __block BOOL success = NO; dispatch_sync(dispatch_get_main_queue(), ^{ NSWindow *window = (__bridge NSWindow *)windowPtr; if (![window isKindOfClass:[NSWindow class]]) { return; } gTrafficLightsX = (CGFloat)x; gTrafficLightsY = (CGFloat)yFromTop; ensureTrafficLightsObserver(window); success = applyTrafficLightsPosition(window, gTrafficLightsX, gTrafficLightsY); if (success) { [window invalidateShadow]; } }); return success; } extern "C" bool setNativeWindowDragRegion(void *windowPtr, double x, double height) { if (windowPtr == nullptr) { return false; } __block BOOL success = NO; dispatch_sync(dispatch_get_main_queue(), ^{ NSWindow *window = (__bridge NSWindow *)windowPtr; if (![window isKindOfClass:[NSWindow class]]) { return; } NSView *contentView = [window contentView]; if (contentView == nil) { return; } CGFloat dragX = MAX(0.0, x); CGFloat dragHeight = MAX(0.0, height); CGFloat dragWidth = MAX(0.0, contentView.bounds.size.width - dragX); if (dragHeight <= 0.0 || dragWidth <= 0.0) { return; } BOOL flipped = [contentView isFlipped]; CGFloat dragY = flipped ? 0.0 : contentView.bounds.size.height - dragHeight; dragY = MAX(0.0, dragY); ElectrobunNativeDragView *dragView = findNativeDragView(contentView); if (dragView == nil) { dragView = [[ElectrobunNativeDragView alloc] initWithFrame:NSZeroRect]; [dragView setIdentifier:kElectrobunNativeDragViewIdentifier]; } [dragView setFrame:NSMakeRect(dragX, dragY, dragWidth, dragHeight)]; [dragView setAutoresizingMask:NSViewWidthSizable]; if ([dragView superview] == nil) { [contentView addSubview:dragView positioned:NSWindowAbove relativeTo:nil]; } success = YES; }); return success; }