Add macOS vibrancy support and related build scripts
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
- Introduced native macOS window effects with a new dynamic library for enhanced UI. - Updated build scripts to compile the macOS effects library and integrate it into the application. - Modified Electron context to apply macOS-specific styles and effects based on the platform. - Enhanced CSS for macOS vibrancy support in the application layout. - Updated configuration files to include new build steps and dependencies for macOS.
This commit is contained in:
parent
f90ef10b78
commit
274852b895
5
.gitignore
vendored
5
.gitignore
vendored
@ -31,4 +31,7 @@ stats.html
|
|||||||
|
|
||||||
test-results.xml
|
test-results.xml
|
||||||
|
|
||||||
dist/*
|
dist/*
|
||||||
|
|
||||||
|
# Native macOS vibrancy (built by scripts/build-macos-effects.mjs)
|
||||||
|
src/bun/libMacWindowEffects.dylib
|
||||||
@ -108,6 +108,43 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Native macOS vibrancy — transparent web content over NSVisualEffectView */
|
||||||
|
html.macos-vibrancy {
|
||||||
|
--macos-window-corner-radius: 15px; /* keep in sync with MAC_WINDOW_CORNER_RADIUS */
|
||||||
|
}
|
||||||
|
|
||||||
|
html.macos-vibrancy,
|
||||||
|
html.macos-vibrancy body,
|
||||||
|
html.macos-vibrancy #root {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.macos-vibrancy .ant-layout.ant-layout-has-sider,
|
||||||
|
html.macos-vibrancy .main-layout {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.macos-vibrancy .light-mode .ant-layout-sider {
|
||||||
|
background: rgba(255, 255, 255, 0.75) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.macos-vibrancy .dark-mode .ant-layout-sider {
|
||||||
|
background: rgba(10, 10, 10, 0.65) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.macos-vibrancy .light-mode .ant-layout-content {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.macos-vibrancy .dark-mode .ant-layout-content {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.macos-vibrancy .ant-layout-sider-light,
|
||||||
|
html.macos-vibrancy .electron-sider .ant-menu-light {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
.electron-navigation {
|
.electron-navigation {
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export default {
|
|||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
"dist/mainview": "views/mainview",
|
"dist/mainview": "views/mainview",
|
||||||
|
"src/bun/libMacWindowEffects.dylib": "bun/libMacWindowEffects.dylib",
|
||||||
},
|
},
|
||||||
watch: ["scripts", "src"],
|
watch: ["scripts", "src"],
|
||||||
watchIgnore: ["dist/**", "build/**", "app_dist/**"],
|
watchIgnore: ["dist/**", "build/**", "app_dist/**"],
|
||||||
|
|||||||
402
native/macos/window-effects.mm
Normal file
402
native/macos/window-effects.mm
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
static NSString *const kElectrobunVibrancyViewIdentifier =
|
||||||
|
@"ElectrobunVibrancyView";
|
||||||
|
static NSString *const kElectrobunNativeDragViewIdentifier =
|
||||||
|
@"ElectrobunNativeDragView";
|
||||||
|
static NSString *const kElectrobunWindowBorderViewIdentifier =
|
||||||
|
@"ElectrobunWindowBorderView";
|
||||||
|
|
||||||
|
static CGFloat gWindowCornerRadius = 15.0;
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
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 void applyWindowCornerRadius(NSWindow *window, CGFloat radius) {
|
||||||
|
gWindowCornerRadius = MAX(0.0, radius);
|
||||||
|
|
||||||
|
NSView *contentView = [window contentView];
|
||||||
|
if (contentView == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentView.wantsLayer = YES;
|
||||||
|
contentView.layer.cornerRadius = gWindowCornerRadius;
|
||||||
|
contentView.layer.masksToBounds = YES;
|
||||||
|
|
||||||
|
NSVisualEffectView *effectView = findVibrancyView(contentView);
|
||||||
|
if (effectView != nil) {
|
||||||
|
effectView.wantsLayer = YES;
|
||||||
|
effectView.layer.cornerRadius = gWindowCornerRadius;
|
||||||
|
effectView.layer.masksToBounds = YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElectrobunWindowBorderView *borderView = findWindowBorderView(contentView);
|
||||||
|
if (borderView != nil) {
|
||||||
|
borderView.cornerRadius = gWindowCornerRadius;
|
||||||
|
[borderView setNeedsDisplay:YES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.cornerRadius = gWindowCornerRadius;
|
||||||
|
[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];
|
||||||
|
}
|
||||||
|
|
||||||
|
[borderView setNeedsDisplay:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
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:NSVisualEffectMaterialUnderWindowBackground];
|
||||||
|
} 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);
|
||||||
|
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSButton *closeButton =
|
||||||
|
[window standardWindowButton:NSWindowCloseButton];
|
||||||
|
NSButton *minimizeButton =
|
||||||
|
[window standardWindowButton:NSWindowMiniaturizeButton];
|
||||||
|
NSButton *zoomButton = [window standardWindowButton:NSWindowZoomButton];
|
||||||
|
|
||||||
|
if (closeButton == nil || minimizeButton == nil || zoomButton == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSView *buttonContainer = [closeButton superview];
|
||||||
|
if (buttonContainer == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 setFrameOrigin:NSMakePoint(currentX, targetY)];
|
||||||
|
currentX += spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
[buttonContainer setNeedsLayout:YES];
|
||||||
|
[buttonContainer layoutSubtreeIfNeeded];
|
||||||
|
[window invalidateShadow];
|
||||||
|
success = YES;
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@ -94,7 +94,8 @@
|
|||||||
"build:electron": "vite build && electron-builder",
|
"build:electron": "vite build && electron-builder",
|
||||||
"build:cloudflare": "cross-env VITE_DEPLOY_TARGET=cloudflare vite build",
|
"build:cloudflare": "cross-env VITE_DEPLOY_TARGET=cloudflare vite build",
|
||||||
"deploy": "npm run build:cloudflare && wrangler pages deploy --branch main",
|
"deploy": "npm run build:cloudflare && wrangler pages deploy --branch main",
|
||||||
"postinstall": "bun scripts/patch-electrobun-src.mjs",
|
"postinstall": "bun scripts/patch-electrobun-src.mjs && bun scripts/build-macos-effects.mjs",
|
||||||
|
"build:macos-effects": "bun scripts/build-macos-effects.mjs",
|
||||||
"clean": "bun scripts/clean-build.mjs"
|
"clean": "bun scripts/clean-build.mjs"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|||||||
48
scripts/build-macos-effects.mjs
Normal file
48
scripts/build-macos-effects.mjs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const srcFile = path.join(rootDir, "native/macos/window-effects.mm");
|
||||||
|
const outFile = path.join(rootDir, "src/bun/libMacWindowEffects.dylib");
|
||||||
|
|
||||||
|
function createPlaceholder() {
|
||||||
|
mkdirSync(path.dirname(outFile), { recursive: true });
|
||||||
|
writeFileSync(outFile, "");
|
||||||
|
console.log(`build-macos-effects: created placeholder dylib at ${outFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
createPlaceholder();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(srcFile)) {
|
||||||
|
console.error(`build-macos-effects: missing source file ${srcFile}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(path.dirname(outFile), { recursive: true });
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
"xcrun",
|
||||||
|
[
|
||||||
|
"clang++",
|
||||||
|
"-dynamiclib",
|
||||||
|
"-fobjc-arc",
|
||||||
|
"-framework",
|
||||||
|
"Cocoa",
|
||||||
|
srcFile,
|
||||||
|
"-o",
|
||||||
|
outFile,
|
||||||
|
],
|
||||||
|
{ cwd: rootDir, stdio: "inherit" },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { size } = statSync(outFile);
|
||||||
|
console.log(`build-macos-effects: built ${outFile} (${size} bytes)`);
|
||||||
@ -36,6 +36,16 @@ if (buildEnv === "stable" && process.env.ELECTROBUN_OS === "macos") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildMacosEffects = spawnSync(
|
||||||
|
"bun",
|
||||||
|
[path.join(rootDir, "scripts/build-macos-effects.mjs")],
|
||||||
|
{ cwd: rootDir, stdio: "inherit", env: process.env },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (buildMacosEffects.status !== 0) {
|
||||||
|
process.exit(buildMacosEffects.status ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
const prepareIcons = spawnSync(
|
const prepareIcons = spawnSync(
|
||||||
"bun",
|
"bun",
|
||||||
[
|
[
|
||||||
|
|||||||
@ -26,6 +26,16 @@ if (patchResult.status !== 0) {
|
|||||||
process.exit(patchResult.status ?? 1);
|
process.exit(patchResult.status ?? 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const macosEffectsResult = spawnSync(
|
||||||
|
"bun",
|
||||||
|
[path.join(rootDir, "scripts/build-macos-effects.mjs")],
|
||||||
|
{ cwd: rootDir, stdio: "inherit", env: process.env },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (macosEffectsResult.status !== 0) {
|
||||||
|
process.exit(macosEffectsResult.status ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
const ensureCoreEnv = {
|
const ensureCoreEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
ELECTROBUN_TARGET_ARCH: targetArch,
|
ELECTROBUN_TARGET_ARCH: targetArch,
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const DashboardLayout = ({ children }) => {
|
|||||||
<MessageProvider>
|
<MessageProvider>
|
||||||
<Layout
|
<Layout
|
||||||
style={{ height: 'var(--unit-100vh)' }}
|
style={{ height: 'var(--unit-100vh)' }}
|
||||||
className={isDarkMode ? 'dark-mode' : 'light-mode'}
|
className={`${isDarkMode ? 'dark-mode' : 'light-mode'} main-layout`}
|
||||||
>
|
>
|
||||||
<DashboardNavigation />
|
<DashboardNavigation />
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|||||||
@ -118,7 +118,12 @@ const ElectronProvider = ({ children }) => {
|
|||||||
|
|
||||||
if (useElectrobun) {
|
if (useElectrobun) {
|
||||||
desktopAPI.getOsInfo().then((info) => {
|
desktopAPI.getOsInfo().then((info) => {
|
||||||
if (info?.platform) setPlatform(info.platform)
|
if (info?.platform) {
|
||||||
|
setPlatform(info.platform)
|
||||||
|
if (info.platform === 'darwin') {
|
||||||
|
document.documentElement.classList.add('macos-vibrancy')
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
desktopAPI.getWindowState().then(applyWindowState)
|
desktopAPI.getWindowState().then(applyWindowState)
|
||||||
@ -142,7 +147,12 @@ const ElectronProvider = ({ children }) => {
|
|||||||
if (!ipcRenderer) return
|
if (!ipcRenderer) return
|
||||||
|
|
||||||
ipcRenderer.invoke('os-info').then((info) => {
|
ipcRenderer.invoke('os-info').then((info) => {
|
||||||
if (info?.platform) setPlatform(info.platform)
|
if (info?.platform) {
|
||||||
|
setPlatform(info.platform)
|
||||||
|
if (info.platform === 'darwin') {
|
||||||
|
document.documentElement.classList.add('macos-vibrancy')
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcRenderer.invoke('window-state').then(applyWindowState)
|
ipcRenderer.invoke('window-state').then(applyWindowState)
|
||||||
|
|||||||
105
src/desktop/macos-window-effects.js
Normal file
105
src/desktop/macos-window-effects.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { dlopen, FFIType } from 'bun:ffi'
|
||||||
|
import { existsSync, statSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const MAC_TRAFFIC_LIGHTS_X = 14
|
||||||
|
const MAC_TRAFFIC_LIGHTS_Y = 12
|
||||||
|
export const MAC_WINDOW_CORNER_RADIUS = 15
|
||||||
|
|
||||||
|
function resolveDylibPath() {
|
||||||
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const candidates = [
|
||||||
|
path.join(moduleDir, '../bun/libMacWindowEffects.dylib'),
|
||||||
|
path.join(moduleDir, 'libMacWindowEffects.dylib')
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!existsSync(candidate)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (statSync(candidate).size > 0) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore unreadable paths and keep searching.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyMacOSWindowEffects(mainWindow) {
|
||||||
|
if (process.platform !== 'darwin' || !mainWindow?.ptr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dylibPath = resolveDylibPath()
|
||||||
|
if (!dylibPath) {
|
||||||
|
console.warn(
|
||||||
|
'macOS vibrancy: native effects library not found; using transparent window only.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lib = dlopen(dylibPath, {
|
||||||
|
enableWindowVibrancy: {
|
||||||
|
args: [FFIType.ptr, FFIType.f64],
|
||||||
|
returns: FFIType.bool
|
||||||
|
},
|
||||||
|
ensureWindowShadow: {
|
||||||
|
args: [FFIType.ptr],
|
||||||
|
returns: FFIType.bool
|
||||||
|
},
|
||||||
|
setWindowCornerRadius: {
|
||||||
|
args: [FFIType.ptr, FFIType.f64],
|
||||||
|
returns: FFIType.bool
|
||||||
|
},
|
||||||
|
setWindowTrafficLightsPosition: {
|
||||||
|
args: [FFIType.ptr, FFIType.f64, FFIType.f64],
|
||||||
|
returns: FFIType.bool
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const vibrancyEnabled = lib.symbols.enableWindowVibrancy(
|
||||||
|
mainWindow.ptr,
|
||||||
|
MAC_WINDOW_CORNER_RADIUS
|
||||||
|
)
|
||||||
|
const shadowEnabled = lib.symbols.ensureWindowShadow(mainWindow.ptr)
|
||||||
|
const alignButtons = () =>
|
||||||
|
lib.symbols.setWindowTrafficLightsPosition(
|
||||||
|
mainWindow.ptr,
|
||||||
|
MAC_TRAFFIC_LIGHTS_X,
|
||||||
|
MAC_TRAFFIC_LIGHTS_Y
|
||||||
|
)
|
||||||
|
const refreshWindowChrome = () => {
|
||||||
|
lib.symbols.setWindowCornerRadius(
|
||||||
|
mainWindow.ptr,
|
||||||
|
MAC_WINDOW_CORNER_RADIUS
|
||||||
|
)
|
||||||
|
alignButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonsAlignedNow = alignButtons()
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshWindowChrome()
|
||||||
|
}, 120)
|
||||||
|
|
||||||
|
mainWindow.on?.('resize', () => {
|
||||||
|
refreshWindowChrome()
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`macOS vibrancy applied (vibrancy=${vibrancyEnabled}, shadow=${shadowEnabled}, trafficLights=${buttonsAlignedNow}, cornerRadius=${MAC_WINDOW_CORNER_RADIUS})`
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
'macOS vibrancy: failed to apply native window effects:',
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,10 @@
|
|||||||
import Electrobun, { BrowserWindow, Updater, Utils } from "electrobun/bun";
|
import Electrobun, { BrowserWindow, Updater, Utils } from "electrobun/bun";
|
||||||
import { applyApplicationMenu, setupApplicationMenuEvents } from "./menu.js";
|
import { applyApplicationMenu, setupApplicationMenuEvents } from "./menu.js";
|
||||||
|
import { applyMacOSWindowEffects } from "./macos-window-effects.js";
|
||||||
import { sendToRenderer, setMessageSender } from "./notify.js";
|
import { sendToRenderer, setMessageSender } from "./notify.js";
|
||||||
|
|
||||||
|
const isMacOS = process.platform === "darwin";
|
||||||
|
|
||||||
const DEV_SERVER_PORT = 5780;
|
const DEV_SERVER_PORT = 5780;
|
||||||
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
|
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
|
||||||
const PROTOCOL_PREFIX = "farmcontrol://";
|
const PROTOCOL_PREFIX = "farmcontrol://";
|
||||||
@ -127,6 +130,7 @@ export async function createMainWindow(rpc) {
|
|||||||
rpc,
|
rpc,
|
||||||
titleBarStyle: "hiddenInset",
|
titleBarStyle: "hiddenInset",
|
||||||
trafficLightOffset: { x: 14, y: 12 },
|
trafficLightOffset: { x: 14, y: 12 },
|
||||||
|
...(isMacOS ? { transparent: true } : {}),
|
||||||
frame: {
|
frame: {
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
@ -135,6 +139,10 @@ export async function createMainWindow(rpc) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isMacOS) {
|
||||||
|
applyMacOSWindowEffects(mainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
setupMainWindowMessaging(mainWindow);
|
setupMainWindowMessaging(mainWindow);
|
||||||
applyApplicationMenu();
|
applyApplicationMenu();
|
||||||
setupApplicationMenuEvents({
|
setupApplicationMenuEvents({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user