|
13 | 13 | // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= |
14 | 14 |
|
15 | 15 | import { BrowserWindow } from 'electron'; |
| 16 | +import log from 'electron-log'; |
16 | 17 | import koffi from 'koffi'; |
17 | 18 | import os from 'os'; |
18 | 19 |
|
19 | | -// NSVisualEffectView material constants (enum values) |
20 | | -export const NSVisualEffectMaterial = { |
21 | | - Titlebar: 3, |
22 | | - Selection: 4, |
23 | | - Menu: 5, |
24 | | - Popover: 6, |
25 | | - Sidebar: 7, |
26 | | - HeaderView: 10, |
27 | | - Sheet: 11, |
28 | | - WindowBackground: 12, |
29 | | - HUDWindow: 13, |
30 | | - FullScreenUI: 15, |
31 | | - ToolTip: 17, |
32 | | - ContentBackground: 18, |
33 | | - UnderWindowBackground: 21, |
34 | | - UnderPageBackground: 22, |
35 | | -} as const; |
| 20 | +type SetRoundedCorners = (window: BrowserWindow, radius?: number) => void; |
36 | 21 |
|
37 | | -export type MaterialType = keyof typeof NSVisualEffectMaterial; |
38 | | - |
39 | | -// Interface for our module functions |
40 | | -interface MacWindowUtils { |
41 | | - setVibrancy: (window: BrowserWindow, material?: MaterialType) => void; |
42 | | - setRoundedCorners: (window: BrowserWindow, radius?: number) => void; |
43 | | - setTransparentTitlebar: (window: BrowserWindow) => void; |
44 | | -} |
45 | | - |
46 | | -let utils: MacWindowUtils; |
| 22 | +let setRoundedCorners: SetRoundedCorners = () => {}; |
47 | 23 |
|
48 | 24 | if (os.platform() === 'darwin') { |
49 | 25 | try { |
50 | 26 | const objc = koffi.load('libobjc.A.dylib'); |
51 | 27 |
|
52 | | - // Types |
53 | 28 | const Ptr = 'size_t'; |
54 | 29 |
|
55 | | - const objc_getClass = objc.func('objc_getClass', Ptr, ['string']); |
56 | 30 | const sel_registerName = objc.func('sel_registerName', Ptr, ['string']); |
57 | 31 | const objc_msgSend = objc.func('objc_msgSend', Ptr, [Ptr, Ptr]); |
58 | | - const objc_msgSend_long = objc.func('objc_msgSend', Ptr, [ |
| 32 | + const objc_msgSend_bool = objc.func('objc_msgSend', Ptr, [ |
59 | 33 | Ptr, |
60 | 34 | Ptr, |
61 | | - 'long', |
| 35 | + 'bool', |
62 | 36 | ]); |
63 | 37 | const objc_msgSend_double = objc.func('objc_msgSend', Ptr, [ |
64 | 38 | Ptr, |
65 | 39 | Ptr, |
66 | 40 | 'double', |
67 | 41 | ]); |
68 | | - const objc_msgSend_bool = objc.func('objc_msgSend', Ptr, [ |
69 | | - Ptr, |
70 | | - Ptr, |
71 | | - 'bool', |
72 | | - ]); |
73 | | - |
74 | | - const NSRect = koffi.struct('NSRect', { |
75 | | - x: 'double', |
76 | | - y: 'double', |
77 | | - width: 'double', |
78 | | - height: 'double', |
79 | | - }); |
80 | | - |
81 | | - const NSVisualEffectBlendingMode = { |
82 | | - BehindWindow: 0, |
83 | | - WithinWindow: 1, |
84 | | - }; |
85 | | - |
86 | | - utils = { |
87 | | - setVibrancy: ( |
88 | | - window: BrowserWindow, |
89 | | - material: MaterialType = 'HUDWindow' |
90 | | - ) => { |
91 | | - try { |
92 | | - const windowHandle = window.getNativeWindowHandle(); |
93 | | - if (windowHandle.length === 0) return; |
94 | | - |
95 | | - // Electron calls valid native handle returns the NSView (BridgedContentView) on macOS |
96 | | - const nsViewPtr = windowHandle.readBigUInt64LE(); |
97 | | - if (!nsViewPtr) return; |
98 | | - |
99 | | - // Selectors |
100 | | - const selAlloc = sel_registerName('alloc'); |
101 | | - const selInit = sel_registerName('init'); |
102 | | - const selSetMaterial = sel_registerName('setMaterial:'); |
103 | | - const selSetBlendingMode = sel_registerName('setBlendingMode:'); |
104 | | - const selSetState = sel_registerName('setState:'); |
105 | | - const selSetAutoresizingMask = sel_registerName( |
106 | | - 'setAutoresizingMask:' |
107 | | - ); |
108 | | - const selSetFrame = sel_registerName('setFrame:'); |
109 | | - const selAddSubview = sel_registerName( |
110 | | - 'addSubview:positioned:relativeTo:' |
111 | | - ); |
112 | 42 |
|
113 | | - const NSVisualEffectViewClass = objc_getClass('NSVisualEffectView'); |
114 | | - if (!NSVisualEffectViewClass) return; |
| 43 | + setRoundedCorners = (window: BrowserWindow, radius = 20) => { |
| 44 | + try { |
| 45 | + const windowHandle = window.getNativeWindowHandle(); |
| 46 | + if (!windowHandle?.length) return; |
115 | 47 |
|
116 | | - // Allocation |
117 | | - const visualEffectView = objc_msgSend( |
118 | | - NSVisualEffectViewClass, |
119 | | - selAlloc |
120 | | - ); |
121 | | - objc_msgSend(visualEffectView, selInit); |
| 48 | + const nsViewPtr = windowHandle.readBigUInt64LE(); |
| 49 | + if (!nsViewPtr) return; |
122 | 50 |
|
123 | | - const materialValue = |
124 | | - NSVisualEffectMaterial[material] || |
125 | | - NSVisualEffectMaterial.HUDWindow; |
| 51 | + const selLayer = sel_registerName('layer'); |
| 52 | + const selSetWantsLayer = sel_registerName('setWantsLayer:'); |
| 53 | + const selSetCornerRadius = sel_registerName('setCornerRadius:'); |
| 54 | + const selSetMasksToBounds = sel_registerName('setMasksToBounds:'); |
126 | 55 |
|
127 | | - // Configuration |
128 | | - objc_msgSend_long(visualEffectView, selSetMaterial, materialValue); |
129 | | - objc_msgSend_long( |
130 | | - visualEffectView, |
131 | | - selSetBlendingMode, |
132 | | - NSVisualEffectBlendingMode.WithinWindow |
133 | | - ); |
134 | | - objc_msgSend_long(visualEffectView, selSetState, 1); |
135 | | - objc_msgSend_long(visualEffectView, selSetAutoresizingMask, 18); |
| 56 | + objc_msgSend_bool(nsViewPtr, selSetWantsLayer, true); |
136 | 57 |
|
137 | | - // Frame |
138 | | - const bounds = window.getBounds(); |
139 | | - const viewFrame = { |
140 | | - x: 0, |
141 | | - y: 0, |
142 | | - width: bounds.width, |
143 | | - height: bounds.height, |
144 | | - }; |
145 | | - |
146 | | - const objc_msgSend_frame = objc.func('objc_msgSend', 'void', [ |
147 | | - Ptr, |
148 | | - Ptr, |
149 | | - NSRect, |
150 | | - ]); |
151 | | - objc_msgSend_frame(visualEffectView, selSetFrame, viewFrame); |
152 | | - |
153 | | - // Add Subview to the CONTENT VIEW (which we already have as nsViewPtr) |
154 | | - const objc_msgSend_positioned = objc.func('objc_msgSend', 'void', [ |
155 | | - Ptr, |
156 | | - Ptr, |
157 | | - Ptr, |
158 | | - 'long', |
159 | | - Ptr, |
160 | | - ]); |
161 | | - objc_msgSend_positioned( |
162 | | - nsViewPtr, |
163 | | - selAddSubview, |
164 | | - visualEffectView, |
165 | | - -1, |
166 | | - 0 |
167 | | - ); // -1 = NSWindowBelow |
168 | | - |
169 | | - console.log(`[MacOS] Vibrancy applied successfully`); |
170 | | - } catch (error) { |
171 | | - console.error('[MacOS] Error applying vibrancy:', error); |
172 | | - } |
173 | | - }, |
174 | | - |
175 | | - setRoundedCorners: (window: BrowserWindow, radius = 20) => { |
176 | | - try { |
177 | | - const windowHandle = window.getNativeWindowHandle(); |
178 | | - const nsViewPtr = windowHandle.readBigUInt64LE(); |
179 | | - |
180 | | - const selLayer = sel_registerName('layer'); |
181 | | - const selSetWantsLayer = sel_registerName('setWantsLayer:'); |
182 | | - const selSetCornerRadius = sel_registerName('setCornerRadius:'); |
183 | | - const selSetMasksToBounds = sel_registerName('setMasksToBounds:'); |
184 | | - |
185 | | - // Ensure layer-backing |
186 | | - objc_msgSend_bool(nsViewPtr, selSetWantsLayer, true); |
187 | | - |
188 | | - // Get layer |
189 | | - const nsLayer = objc_msgSend(nsViewPtr, selLayer); |
190 | | - if (!nsLayer) return console.error('[MacOS] Failed to get layer'); |
191 | | - |
192 | | - // Apply Corner Radius |
193 | | - objc_msgSend_double(nsLayer, selSetCornerRadius, radius); |
194 | | - objc_msgSend_bool(nsLayer, selSetMasksToBounds, true); |
195 | | - |
196 | | - console.log(`[MacOS] Rounded corners applied: ${radius}`); |
197 | | - } catch (error) { |
198 | | - console.error('[MacOS] Error applying rounded corners:', error); |
| 58 | + const nsLayer = objc_msgSend(nsViewPtr, selLayer); |
| 59 | + if (!nsLayer) { |
| 60 | + log.error('[MacOS] Failed to get layer for rounded corners'); |
| 61 | + return; |
199 | 62 | } |
200 | | - }, |
201 | | - |
202 | | - setTransparentTitlebar: (window: BrowserWindow) => { |
203 | | - try { |
204 | | - const windowHandle = window.getNativeWindowHandle(); |
205 | | - const nsViewPtr = windowHandle.readBigUInt64LE(); |
206 | | - |
207 | | - // We have the View, we need the Window |
208 | | - const selWindow = sel_registerName('window'); |
209 | | - const nsWindowPtr = objc_msgSend(nsViewPtr, selWindow); |
210 | 63 |
|
211 | | - if (!nsWindowPtr) |
212 | | - return console.error('[MacOS] Failed to get NSWindow from NSView'); |
213 | | - |
214 | | - const selSetTitlebarAppearsTransparent = sel_registerName( |
215 | | - 'setTitlebarAppearsTransparent:' |
216 | | - ); |
217 | | - objc_msgSend_bool( |
218 | | - nsWindowPtr, |
219 | | - selSetTitlebarAppearsTransparent, |
220 | | - true |
221 | | - ); |
222 | | - |
223 | | - console.log('[MacOS] Transparent titlebar applied'); |
224 | | - } catch (error) { |
225 | | - console.error('[MacOS] Error setting transparent titlebar:', error); |
226 | | - } |
227 | | - }, |
| 64 | + objc_msgSend_double(nsLayer, selSetCornerRadius, radius); |
| 65 | + objc_msgSend_bool(nsLayer, selSetMasksToBounds, true); |
| 66 | + } catch (error) { |
| 67 | + log.error('[MacOS] Error applying rounded corners:', error); |
| 68 | + } |
228 | 69 | }; |
229 | 70 | } catch (e) { |
230 | | - console.error('[MacOS] Failed to load native libraries:', e); |
231 | | - utils = { |
232 | | - setVibrancy: () => {}, |
233 | | - setRoundedCorners: () => {}, |
234 | | - setTransparentTitlebar: () => {}, |
235 | | - }; |
| 71 | + log.error( |
| 72 | + '[MacOS] Failed to load native libraries for rounded corners:', |
| 73 | + e |
| 74 | + ); |
236 | 75 | } |
237 | | -} else { |
238 | | - utils = { |
239 | | - setVibrancy: () => {}, |
240 | | - setRoundedCorners: () => {}, |
241 | | - setTransparentTitlebar: () => {}, |
242 | | - }; |
243 | 76 | } |
244 | 77 |
|
245 | | -export const { setVibrancy, setRoundedCorners, setTransparentTitlebar } = utils; |
| 78 | +export { setRoundedCorners }; |
0 commit comments