@zakim24/electron-liquid-glass 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +247 -0
- package/build/Release/liquidglass.node +0 -0
- package/dist/index.cjs +129 -0
- package/dist/index.d.cts +71 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +107 -0
- package/js/index.ts +117 -0
- package/js/native-loader.ts +10 -0
- package/js/variants.ts +28 -0
- package/package.json +88 -0
- package/prebuilds/darwin-arm64/node.napi.node +0 -0
- package/src/glass_effect.mm +256 -0
- package/src/liquidglass.cc +139 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Meridius Labs
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the βSoftwareβ), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED βAS ISβ, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# electron-liquid-glass
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<img width="387" alt="image" src="https://github.com/user-attachments/assets/3c3c9ea6-2663-4292-b812-a630c2c3f65b" />
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
**Modern macOS glass effects for Electron applications**
|
|
14
|
+
|
|
15
|
+
_πͺ NATIVE `NSGlassEffectView` integration with ZERO CSS hacks_
|
|
16
|
+
|
|
17
|
+
[Installation](#-installation) β’ [Quick Start](#-quick-start) β’ [API](#-api-reference) β’ [Examples](examples/) β’ [Contributing](#-contributing)
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## β¨ Features
|
|
24
|
+
|
|
25
|
+
- πͺ **Native Glass Effects** - Real `NSGlassEffectView` integration, not CSS approximations
|
|
26
|
+
- β‘ **Zero Configuration** - Works out of the box with any Electron app
|
|
27
|
+
- π¨ **Fully Customizable** - Corner radius, tint colors, and glass variants
|
|
28
|
+
- π¦ **Modern Package** - Dual ESM/CommonJS support with TypeScript declarations
|
|
29
|
+
- π§ **Pre-built Binaries** - No compilation required for standard setups
|
|
30
|
+
- π **Auto Dark Mode** - Automatically adapts to system appearance changes
|
|
31
|
+
|
|
32
|
+
## π Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# npm
|
|
36
|
+
npm install electron-liquid-glass
|
|
37
|
+
|
|
38
|
+
# yarn
|
|
39
|
+
yarn add electron-liquid-glass
|
|
40
|
+
|
|
41
|
+
# pnpm
|
|
42
|
+
pnpm add electron-liquid-glass
|
|
43
|
+
|
|
44
|
+
# bun
|
|
45
|
+
bun add electron-liquid-glass
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Requirements
|
|
49
|
+
|
|
50
|
+
- **macOS 26+** (Tahoe or later)
|
|
51
|
+
- **Electron 30+**
|
|
52
|
+
- **Node.js 22+**
|
|
53
|
+
|
|
54
|
+
> **Note**: This package only works on macOS. On other platforms, it provides safe no-op fallbacks.
|
|
55
|
+
|
|
56
|
+
## π― Quick Start
|
|
57
|
+
|
|
58
|
+
### Basic Usage
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
import { app, BrowserWindow } from "electron";
|
|
62
|
+
import liquidGlass from "electron-liquid-glass";
|
|
63
|
+
|
|
64
|
+
app.whenReady().then(() => {
|
|
65
|
+
const win = new BrowserWindow({
|
|
66
|
+
width: 800,
|
|
67
|
+
height: 600,
|
|
68
|
+
|
|
69
|
+
vibrancy: false, // <-- βββ do NOT set vibrancy alongside with liquid glass, it will override and look blurry
|
|
70
|
+
|
|
71
|
+
transparent: true, // <-- This MUST be true
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
win.setWindowButtonVisibility(true); // <-- β
This is required to show the window buttons
|
|
75
|
+
|
|
76
|
+
win.loadFile("index.html");
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* πͺ Apply glass effect after content loads πͺ
|
|
80
|
+
*/
|
|
81
|
+
win.webContents.once("did-finish-load", () => {
|
|
82
|
+
// πͺ Apply effect, get handle
|
|
83
|
+
const glassId = liquidGlass.addView(win.getNativeWindowHandle(), {
|
|
84
|
+
/* options */
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Experimental, undocumented private APIs
|
|
88
|
+
liquidGlass.unstable_setVariant(glassId, 2);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### TypeScript Usage
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { BrowserWindow } from "electron";
|
|
97
|
+
import liquidGlass, { GlassOptions } from "electron-liquid-glass";
|
|
98
|
+
|
|
99
|
+
const options: GlassOptions = {
|
|
100
|
+
cornerRadius: 16, // (optional)
|
|
101
|
+
tintColor: "#44000010", // black tint (optional)
|
|
102
|
+
opaque: true, // add opaque background behind glass (optional)
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
liquidGlass.addView(window.getNativeWindowHandle(), options);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## π API Reference
|
|
109
|
+
|
|
110
|
+
### `liquidGlass.addView(handle, options?)`
|
|
111
|
+
|
|
112
|
+
Applies a glass effect to an Electron window.
|
|
113
|
+
|
|
114
|
+
**Parameters:**
|
|
115
|
+
|
|
116
|
+
- `handle: Buffer` - The native window handle from `BrowserWindow.getNativeWindowHandle()`
|
|
117
|
+
- `options?: GlassOptions` - Configuration options
|
|
118
|
+
|
|
119
|
+
**Returns:** `number` - A unique view ID for future operations
|
|
120
|
+
|
|
121
|
+
### `GlassOptions`
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
interface GlassOptions {
|
|
125
|
+
cornerRadius?: number; // Corner radius in pixels (default: 0)
|
|
126
|
+
tintColor?: string; // Hex color with optional alpha (#RRGGBB or #RRGGBBAA)
|
|
127
|
+
opaque?: boolean; // Add opaque background behind glass (default: false)
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### UNDOCUMENTED EXPERIMENTAL METHODS
|
|
134
|
+
|
|
135
|
+
> β οΈ **Warning**: DO NOT USE IN PROD. These methods use private macOS APIs and may change in future versions.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Glass variants (number) (0-15, 19 are functional)
|
|
139
|
+
liquidGlass.unstable_setVariant(glassId, 2);
|
|
140
|
+
|
|
141
|
+
// Scrim overlay (0 = off, 1 = on)
|
|
142
|
+
liquidGlass.unstable_setScrim(glassId, 1);
|
|
143
|
+
|
|
144
|
+
// Subdued state (0 = normal, 1 = subdued)
|
|
145
|
+
liquidGlass.unstable_setSubdued(glassId, 1);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## π§ Development
|
|
149
|
+
|
|
150
|
+
### Building from Source
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Clone the repository
|
|
154
|
+
git clone https://github.com/meridius-labs/electron-liquid-glass.git
|
|
155
|
+
cd electron-liquid-glass
|
|
156
|
+
|
|
157
|
+
# Install dependencies
|
|
158
|
+
bun install
|
|
159
|
+
|
|
160
|
+
# Build native module
|
|
161
|
+
bun run build:native
|
|
162
|
+
|
|
163
|
+
# Build TypeScript library
|
|
164
|
+
bun run build
|
|
165
|
+
|
|
166
|
+
# Build everything
|
|
167
|
+
bun run build:all
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Rebuilding for Custom Electron
|
|
171
|
+
|
|
172
|
+
If you're using a custom Electron version:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npx electron-rebuild -f -w electron-liquid-glass
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Project Structure
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
electron-liquid-glass/
|
|
182
|
+
βββ src/ # Native C++ source code
|
|
183
|
+
β βββ glass_effect.mm # Objective-C++ implementation
|
|
184
|
+
β βββ liquidglass.cc # Node.js addon bindings
|
|
185
|
+
βββ js/ # TypeScript source
|
|
186
|
+
β βββ index.ts # Main library code
|
|
187
|
+
β βββ native-loader.ts # Native module loader
|
|
188
|
+
βββ dist/ # Built library (generated)
|
|
189
|
+
βββ examples/ # Example applications
|
|
190
|
+
βββ prebuilds/ # Pre-built binaries
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## ποΈ How It Works
|
|
194
|
+
|
|
195
|
+
1. **Native Integration**: Uses Objective-C++ to create `NSGlassEffectView` instances
|
|
196
|
+
2. **View Hierarchy**: Inserts glass views behind your web content, not over it
|
|
197
|
+
3. **Automatic Updates**: Listens for system appearance changes to keep effects in sync
|
|
198
|
+
4. **Memory Management**: Properly manages native view lifecycle
|
|
199
|
+
|
|
200
|
+
### Technical Details
|
|
201
|
+
|
|
202
|
+
- **Primary**: Uses `NSGlassEffectView` API when available
|
|
203
|
+
- **Fallback**: Falls back to public `NSVisualEffectView` on older systems
|
|
204
|
+
- **Performance**: Minimal overhead, native rendering performance
|
|
205
|
+
- **Compatibility**: Works with all Electron window configurations
|
|
206
|
+
|
|
207
|
+
## π€ Contributing
|
|
208
|
+
|
|
209
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
210
|
+
|
|
211
|
+
### Development Setup
|
|
212
|
+
|
|
213
|
+
1. Fork the repository
|
|
214
|
+
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
|
215
|
+
3. Make your changes and test thoroughly
|
|
216
|
+
4. Commit with conventional commits: `git commit -m "feat: add amazing feature"`
|
|
217
|
+
5. Push and create a Pull Request
|
|
218
|
+
|
|
219
|
+
### Reporting Issues
|
|
220
|
+
|
|
221
|
+
- Use the [issue tracker](https://github.com/meridius-labs/electron-liquid-glass/issues)
|
|
222
|
+
- Include your macOS version, Electron version, and Node.js version
|
|
223
|
+
- Provide a minimal reproduction case when possible
|
|
224
|
+
|
|
225
|
+
## π Roadmap
|
|
226
|
+
|
|
227
|
+
- [ ] **View Management** - Remove and update existing glass views
|
|
228
|
+
|
|
229
|
+
## π Acknowledgments
|
|
230
|
+
|
|
231
|
+
- Apple's private `NSGlassEffectView` API documentation (reverse-engineered)
|
|
232
|
+
- The Electron team for excellent native integration capabilities
|
|
233
|
+
- Contributors and users who help improve this library
|
|
234
|
+
|
|
235
|
+
## π License
|
|
236
|
+
|
|
237
|
+
MIT Β© [Meridius Labs](https://github.com/meridius-labs) 2025
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
<div align="center">
|
|
242
|
+
|
|
243
|
+
**Made with β€οΈ for the Electron community**
|
|
244
|
+
|
|
245
|
+
[β Star on GitHub](https://github.com/meridius-labs/electron-liquid-glass) β’ [π Report Bug](https://github.com/meridius-labs/electron-liquid-glass/issues) β’ [π‘ Request Feature](https://github.com/meridius-labs/electron-liquid-glass/issues)
|
|
246
|
+
|
|
247
|
+
</div>
|
|
Binary file
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
let events = require("events");
|
|
25
|
+
events = __toESM(events);
|
|
26
|
+
let child_process = require("child_process");
|
|
27
|
+
child_process = __toESM(child_process);
|
|
28
|
+
let url = require("url");
|
|
29
|
+
url = __toESM(url);
|
|
30
|
+
let path = require("path");
|
|
31
|
+
path = __toESM(path);
|
|
32
|
+
|
|
33
|
+
//#region js/variants.ts
|
|
34
|
+
const GlassMaterialVariant = {
|
|
35
|
+
regular: 0,
|
|
36
|
+
clear: 1,
|
|
37
|
+
dock: 2,
|
|
38
|
+
appIcons: 3,
|
|
39
|
+
widgets: 4,
|
|
40
|
+
text: 5,
|
|
41
|
+
avplayer: 6,
|
|
42
|
+
facetime: 7,
|
|
43
|
+
controlCenter: 8,
|
|
44
|
+
notificationCenter: 9,
|
|
45
|
+
monogram: 10,
|
|
46
|
+
bubbles: 11,
|
|
47
|
+
identity: 12,
|
|
48
|
+
focusBorder: 13,
|
|
49
|
+
focusPlatter: 14,
|
|
50
|
+
keyboard: 15,
|
|
51
|
+
sidebar: 16,
|
|
52
|
+
abuttedSidebar: 17,
|
|
53
|
+
inspector: 18,
|
|
54
|
+
control: 19,
|
|
55
|
+
loupe: 20,
|
|
56
|
+
slider: 21,
|
|
57
|
+
camera: 22,
|
|
58
|
+
cartouchePopover: 23
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region js/native-loader.ts
|
|
63
|
+
const __filename$1 = (0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
64
|
+
const __dirname$1 = (0, path.dirname)(__filename$1);
|
|
65
|
+
const nodeGypBuild = require("node-gyp-build");
|
|
66
|
+
var native_loader_default = nodeGypBuild((0, path.join)(__dirname$1, ".."));
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region js/index.ts
|
|
70
|
+
var LiquidGlass = class extends events.EventEmitter {
|
|
71
|
+
_addon;
|
|
72
|
+
_isGlassSupported;
|
|
73
|
+
GlassMaterialVariant = GlassMaterialVariant;
|
|
74
|
+
constructor() {
|
|
75
|
+
super();
|
|
76
|
+
try {
|
|
77
|
+
if (!this.isMacOS()) return;
|
|
78
|
+
this._addon = new native_loader_default.LiquidGlassNative();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error("electron-liquid-glass failed to load its native addon β liquid glass functionality will be disabled.", err);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
isMacOS() {
|
|
84
|
+
return process.platform === "darwin";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if liquid glass is supported on the current platform
|
|
88
|
+
* @returns true if liquid glass is supported on the current platform
|
|
89
|
+
*/
|
|
90
|
+
isGlassSupported() {
|
|
91
|
+
if (this._isGlassSupported !== void 0) return this._isGlassSupported;
|
|
92
|
+
const supported = this.isMacOS() && Number((0, child_process.execSync)("sw_vers -productVersion").toString().trim().split(".")[0]) >= 26;
|
|
93
|
+
this._isGlassSupported = supported;
|
|
94
|
+
return supported;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Wrap the Electron window with a glass / vibrancy view.
|
|
98
|
+
*
|
|
99
|
+
* β οΈ Will gracefully fall back to legacy macOS blur if liquid glass is not supported.
|
|
100
|
+
* @param handle BrowserWindow.getNativeWindowHandle()
|
|
101
|
+
* @param options Glass effect options
|
|
102
|
+
* @returns id β can be used for future API (remove/update), -1 if not supported
|
|
103
|
+
*/
|
|
104
|
+
addView(handle, options = {}) {
|
|
105
|
+
if (!Buffer.isBuffer(handle)) throw new Error("[liquidGlass.addView] handle must be a Buffer");
|
|
106
|
+
if (!this._addon) return -1;
|
|
107
|
+
return this._addon.addView(handle, options);
|
|
108
|
+
}
|
|
109
|
+
setVariant(id, variant) {
|
|
110
|
+
if (!this._addon || typeof this._addon.setVariant !== "function") return;
|
|
111
|
+
this._addon.setVariant(id, variant);
|
|
112
|
+
}
|
|
113
|
+
unstable_setVariant(id, variant) {
|
|
114
|
+
this.setVariant(id, variant);
|
|
115
|
+
}
|
|
116
|
+
unstable_setScrim(id, scrim) {
|
|
117
|
+
if (!this._addon || typeof this._addon.setScrimState !== "function") return;
|
|
118
|
+
this._addon.setScrimState(id, scrim);
|
|
119
|
+
}
|
|
120
|
+
unstable_setSubdued(id, subdued) {
|
|
121
|
+
if (!this._addon || typeof this._addon.setSubduedState !== "function") return;
|
|
122
|
+
this._addon.setSubduedState(id, subdued);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const liquidGlass = new LiquidGlass();
|
|
126
|
+
var js_default = liquidGlass;
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
module.exports = js_default;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
|
|
3
|
+
//#region js/variants.d.ts
|
|
4
|
+
type GlassMaterialVariant = number;
|
|
5
|
+
declare const GlassMaterialVariant: {
|
|
6
|
+
readonly regular: 0;
|
|
7
|
+
readonly clear: 1;
|
|
8
|
+
readonly dock: 2;
|
|
9
|
+
readonly appIcons: 3;
|
|
10
|
+
readonly widgets: 4;
|
|
11
|
+
readonly text: 5;
|
|
12
|
+
readonly avplayer: 6;
|
|
13
|
+
readonly facetime: 7;
|
|
14
|
+
readonly controlCenter: 8;
|
|
15
|
+
readonly notificationCenter: 9;
|
|
16
|
+
readonly monogram: 10;
|
|
17
|
+
readonly bubbles: 11;
|
|
18
|
+
readonly identity: 12;
|
|
19
|
+
readonly focusBorder: 13;
|
|
20
|
+
readonly focusPlatter: 14;
|
|
21
|
+
readonly keyboard: 15;
|
|
22
|
+
readonly sidebar: 16;
|
|
23
|
+
readonly abuttedSidebar: 17;
|
|
24
|
+
readonly inspector: 18;
|
|
25
|
+
readonly control: 19;
|
|
26
|
+
readonly loupe: 20;
|
|
27
|
+
readonly slider: 21;
|
|
28
|
+
readonly camera: 22;
|
|
29
|
+
readonly cartouchePopover: 23;
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region js/index.d.ts
|
|
33
|
+
interface GlassOptions {
|
|
34
|
+
cornerRadius?: number;
|
|
35
|
+
tintColor?: string;
|
|
36
|
+
opaque?: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface LiquidGlassNative {
|
|
39
|
+
addView(handle: Buffer, options: GlassOptions): number;
|
|
40
|
+
setVariant(id: number, variant: GlassMaterialVariant): void;
|
|
41
|
+
setScrimState(id: number, scrim: number): void;
|
|
42
|
+
setSubduedState(id: number, subdued: number): void;
|
|
43
|
+
}
|
|
44
|
+
declare class LiquidGlass extends EventEmitter {
|
|
45
|
+
private _addon?;
|
|
46
|
+
private _isGlassSupported;
|
|
47
|
+
readonly GlassMaterialVariant: typeof GlassMaterialVariant;
|
|
48
|
+
constructor();
|
|
49
|
+
private isMacOS;
|
|
50
|
+
/**
|
|
51
|
+
* Check if liquid glass is supported on the current platform
|
|
52
|
+
* @returns true if liquid glass is supported on the current platform
|
|
53
|
+
*/
|
|
54
|
+
isGlassSupported(): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Wrap the Electron window with a glass / vibrancy view.
|
|
57
|
+
*
|
|
58
|
+
* β οΈ Will gracefully fall back to legacy macOS blur if liquid glass is not supported.
|
|
59
|
+
* @param handle BrowserWindow.getNativeWindowHandle()
|
|
60
|
+
* @param options Glass effect options
|
|
61
|
+
* @returns id β can be used for future API (remove/update), -1 if not supported
|
|
62
|
+
*/
|
|
63
|
+
addView(handle: Buffer, options?: GlassOptions): number;
|
|
64
|
+
private setVariant;
|
|
65
|
+
unstable_setVariant(id: number, variant: GlassMaterialVariant): void;
|
|
66
|
+
unstable_setScrim(id: number, scrim: number): void;
|
|
67
|
+
unstable_setSubdued(id: number, subdued: number): void;
|
|
68
|
+
}
|
|
69
|
+
declare const liquidGlass: LiquidGlass;
|
|
70
|
+
//#endregion
|
|
71
|
+
export { GlassOptions, LiquidGlassNative, liquidGlass as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
|
|
3
|
+
//#region js/variants.d.ts
|
|
4
|
+
type GlassMaterialVariant = number;
|
|
5
|
+
declare const GlassMaterialVariant: {
|
|
6
|
+
readonly regular: 0;
|
|
7
|
+
readonly clear: 1;
|
|
8
|
+
readonly dock: 2;
|
|
9
|
+
readonly appIcons: 3;
|
|
10
|
+
readonly widgets: 4;
|
|
11
|
+
readonly text: 5;
|
|
12
|
+
readonly avplayer: 6;
|
|
13
|
+
readonly facetime: 7;
|
|
14
|
+
readonly controlCenter: 8;
|
|
15
|
+
readonly notificationCenter: 9;
|
|
16
|
+
readonly monogram: 10;
|
|
17
|
+
readonly bubbles: 11;
|
|
18
|
+
readonly identity: 12;
|
|
19
|
+
readonly focusBorder: 13;
|
|
20
|
+
readonly focusPlatter: 14;
|
|
21
|
+
readonly keyboard: 15;
|
|
22
|
+
readonly sidebar: 16;
|
|
23
|
+
readonly abuttedSidebar: 17;
|
|
24
|
+
readonly inspector: 18;
|
|
25
|
+
readonly control: 19;
|
|
26
|
+
readonly loupe: 20;
|
|
27
|
+
readonly slider: 21;
|
|
28
|
+
readonly camera: 22;
|
|
29
|
+
readonly cartouchePopover: 23;
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region js/index.d.ts
|
|
33
|
+
interface GlassOptions {
|
|
34
|
+
cornerRadius?: number;
|
|
35
|
+
tintColor?: string;
|
|
36
|
+
opaque?: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface LiquidGlassNative {
|
|
39
|
+
addView(handle: Buffer, options: GlassOptions): number;
|
|
40
|
+
setVariant(id: number, variant: GlassMaterialVariant): void;
|
|
41
|
+
setScrimState(id: number, scrim: number): void;
|
|
42
|
+
setSubduedState(id: number, subdued: number): void;
|
|
43
|
+
}
|
|
44
|
+
declare class LiquidGlass extends EventEmitter {
|
|
45
|
+
private _addon?;
|
|
46
|
+
private _isGlassSupported;
|
|
47
|
+
readonly GlassMaterialVariant: typeof GlassMaterialVariant;
|
|
48
|
+
constructor();
|
|
49
|
+
private isMacOS;
|
|
50
|
+
/**
|
|
51
|
+
* Check if liquid glass is supported on the current platform
|
|
52
|
+
* @returns true if liquid glass is supported on the current platform
|
|
53
|
+
*/
|
|
54
|
+
isGlassSupported(): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Wrap the Electron window with a glass / vibrancy view.
|
|
57
|
+
*
|
|
58
|
+
* β οΈ Will gracefully fall back to legacy macOS blur if liquid glass is not supported.
|
|
59
|
+
* @param handle BrowserWindow.getNativeWindowHandle()
|
|
60
|
+
* @param options Glass effect options
|
|
61
|
+
* @returns id β can be used for future API (remove/update), -1 if not supported
|
|
62
|
+
*/
|
|
63
|
+
addView(handle: Buffer, options?: GlassOptions): number;
|
|
64
|
+
private setVariant;
|
|
65
|
+
unstable_setVariant(id: number, variant: GlassMaterialVariant): void;
|
|
66
|
+
unstable_setScrim(id: number, scrim: number): void;
|
|
67
|
+
unstable_setSubdued(id: number, subdued: number): void;
|
|
68
|
+
}
|
|
69
|
+
declare const liquidGlass: LiquidGlass;
|
|
70
|
+
//#endregion
|
|
71
|
+
export { GlassOptions, LiquidGlassNative, liquidGlass as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
|
|
7
|
+
//#region rolldown:runtime
|
|
8
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region js/variants.ts
|
|
12
|
+
const GlassMaterialVariant = {
|
|
13
|
+
regular: 0,
|
|
14
|
+
clear: 1,
|
|
15
|
+
dock: 2,
|
|
16
|
+
appIcons: 3,
|
|
17
|
+
widgets: 4,
|
|
18
|
+
text: 5,
|
|
19
|
+
avplayer: 6,
|
|
20
|
+
facetime: 7,
|
|
21
|
+
controlCenter: 8,
|
|
22
|
+
notificationCenter: 9,
|
|
23
|
+
monogram: 10,
|
|
24
|
+
bubbles: 11,
|
|
25
|
+
identity: 12,
|
|
26
|
+
focusBorder: 13,
|
|
27
|
+
focusPlatter: 14,
|
|
28
|
+
keyboard: 15,
|
|
29
|
+
sidebar: 16,
|
|
30
|
+
abuttedSidebar: 17,
|
|
31
|
+
inspector: 18,
|
|
32
|
+
control: 19,
|
|
33
|
+
loupe: 20,
|
|
34
|
+
slider: 21,
|
|
35
|
+
camera: 22,
|
|
36
|
+
cartouchePopover: 23
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region js/native-loader.ts
|
|
41
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
42
|
+
const __dirname = dirname(__filename);
|
|
43
|
+
const nodeGypBuild = __require("node-gyp-build");
|
|
44
|
+
var native_loader_default = nodeGypBuild(join(__dirname, ".."));
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region js/index.ts
|
|
48
|
+
var LiquidGlass = class extends EventEmitter {
|
|
49
|
+
_addon;
|
|
50
|
+
_isGlassSupported;
|
|
51
|
+
GlassMaterialVariant = GlassMaterialVariant;
|
|
52
|
+
constructor() {
|
|
53
|
+
super();
|
|
54
|
+
try {
|
|
55
|
+
if (!this.isMacOS()) return;
|
|
56
|
+
this._addon = new native_loader_default.LiquidGlassNative();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error("electron-liquid-glass failed to load its native addon β liquid glass functionality will be disabled.", err);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
isMacOS() {
|
|
62
|
+
return process.platform === "darwin";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if liquid glass is supported on the current platform
|
|
66
|
+
* @returns true if liquid glass is supported on the current platform
|
|
67
|
+
*/
|
|
68
|
+
isGlassSupported() {
|
|
69
|
+
if (this._isGlassSupported !== void 0) return this._isGlassSupported;
|
|
70
|
+
const supported = this.isMacOS() && Number(execSync("sw_vers -productVersion").toString().trim().split(".")[0]) >= 26;
|
|
71
|
+
this._isGlassSupported = supported;
|
|
72
|
+
return supported;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Wrap the Electron window with a glass / vibrancy view.
|
|
76
|
+
*
|
|
77
|
+
* β οΈ Will gracefully fall back to legacy macOS blur if liquid glass is not supported.
|
|
78
|
+
* @param handle BrowserWindow.getNativeWindowHandle()
|
|
79
|
+
* @param options Glass effect options
|
|
80
|
+
* @returns id β can be used for future API (remove/update), -1 if not supported
|
|
81
|
+
*/
|
|
82
|
+
addView(handle, options = {}) {
|
|
83
|
+
if (!Buffer.isBuffer(handle)) throw new Error("[liquidGlass.addView] handle must be a Buffer");
|
|
84
|
+
if (!this._addon) return -1;
|
|
85
|
+
return this._addon.addView(handle, options);
|
|
86
|
+
}
|
|
87
|
+
setVariant(id, variant) {
|
|
88
|
+
if (!this._addon || typeof this._addon.setVariant !== "function") return;
|
|
89
|
+
this._addon.setVariant(id, variant);
|
|
90
|
+
}
|
|
91
|
+
unstable_setVariant(id, variant) {
|
|
92
|
+
this.setVariant(id, variant);
|
|
93
|
+
}
|
|
94
|
+
unstable_setScrim(id, scrim) {
|
|
95
|
+
if (!this._addon || typeof this._addon.setScrimState !== "function") return;
|
|
96
|
+
this._addon.setScrimState(id, scrim);
|
|
97
|
+
}
|
|
98
|
+
unstable_setSubdued(id, subdued) {
|
|
99
|
+
if (!this._addon || typeof this._addon.setSubduedState !== "function") return;
|
|
100
|
+
this._addon.setSubduedState(id, subdued);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const liquidGlass = new LiquidGlass();
|
|
104
|
+
var js_default = liquidGlass;
|
|
105
|
+
|
|
106
|
+
//#endregion
|
|
107
|
+
export { js_default as default };
|
package/js/index.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { GlassMaterialVariant } from "./variants.js";
|
|
4
|
+
|
|
5
|
+
// Load the native addon using the 'bindings' module
|
|
6
|
+
// This will look for the compiled .node file in various places
|
|
7
|
+
import native from "./native-loader.js";
|
|
8
|
+
|
|
9
|
+
export interface GlassOptions {
|
|
10
|
+
cornerRadius?: number;
|
|
11
|
+
tintColor?: string;
|
|
12
|
+
opaque?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LiquidGlassNative {
|
|
16
|
+
addView(handle: Buffer, options: GlassOptions): number;
|
|
17
|
+
setVariant(id: number, variant: GlassMaterialVariant): void;
|
|
18
|
+
setScrimState(id: number, scrim: number): void;
|
|
19
|
+
setSubduedState(id: number, subdued: number): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Create a nice JavaScript wrapper
|
|
23
|
+
class LiquidGlass extends EventEmitter {
|
|
24
|
+
private _addon?: LiquidGlassNative;
|
|
25
|
+
private _isGlassSupported: boolean | undefined;
|
|
26
|
+
|
|
27
|
+
// Instance property for easy access to variants
|
|
28
|
+
readonly GlassMaterialVariant: typeof GlassMaterialVariant =
|
|
29
|
+
GlassMaterialVariant;
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
if (!this.isMacOS()) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Native addon uses liquid glass (macOS 26+)
|
|
40
|
+
// or falls back to legacy blur as needed.
|
|
41
|
+
this._addon = new native.LiquidGlassNative();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(
|
|
44
|
+
"electron-liquid-glass failed to load its native addon β liquid glass functionality will be disabled.",
|
|
45
|
+
err
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private isMacOS(): boolean {
|
|
51
|
+
return process.platform === "darwin";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if liquid glass is supported on the current platform
|
|
56
|
+
* @returns true if liquid glass is supported on the current platform
|
|
57
|
+
*/
|
|
58
|
+
public isGlassSupported(): boolean {
|
|
59
|
+
if (this._isGlassSupported !== undefined) return this._isGlassSupported;
|
|
60
|
+
|
|
61
|
+
const supported =
|
|
62
|
+
this.isMacOS() &&
|
|
63
|
+
Number(
|
|
64
|
+
execSync("sw_vers -productVersion").toString().trim().split(".")[0]
|
|
65
|
+
) >= 26;
|
|
66
|
+
|
|
67
|
+
this._isGlassSupported = supported;
|
|
68
|
+
return supported;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Wrap the Electron window with a glass / vibrancy view.
|
|
73
|
+
*
|
|
74
|
+
* β οΈ Will gracefully fall back to legacy macOS blur if liquid glass is not supported.
|
|
75
|
+
* @param handle BrowserWindow.getNativeWindowHandle()
|
|
76
|
+
* @param options Glass effect options
|
|
77
|
+
* @returns id β can be used for future API (remove/update), -1 if not supported
|
|
78
|
+
*/
|
|
79
|
+
addView(handle: Buffer, options: GlassOptions = {}): number {
|
|
80
|
+
if (!Buffer.isBuffer(handle)) {
|
|
81
|
+
throw new Error("[liquidGlass.addView] handle must be a Buffer");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!this._addon) {
|
|
85
|
+
// unavailable on this platform
|
|
86
|
+
return -1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return this._addon.addView(handle, options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private setVariant(id: number, variant: GlassMaterialVariant): void {
|
|
93
|
+
if (!this._addon || typeof this._addon.setVariant !== "function") return;
|
|
94
|
+
this._addon.setVariant(id, variant);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
unstable_setVariant(id: number, variant: GlassMaterialVariant): void {
|
|
98
|
+
this.setVariant(id, variant);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
unstable_setScrim(id: number, scrim: number): void {
|
|
102
|
+
if (!this._addon || typeof this._addon.setScrimState !== "function") return;
|
|
103
|
+
this._addon.setScrimState(id, scrim);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
unstable_setSubdued(id: number, subdued: number): void {
|
|
107
|
+
if (!this._addon || typeof this._addon.setSubduedState !== "function")
|
|
108
|
+
return;
|
|
109
|
+
this._addon.setSubduedState(id, subdued);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Create and export the singleton instance
|
|
114
|
+
// The class constructor handles platform checks internally
|
|
115
|
+
const liquidGlass: LiquidGlass = new LiquidGlass();
|
|
116
|
+
|
|
117
|
+
export default liquidGlass;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { fileURLToPath } from "url";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
|
|
4
|
+
// ESM-compatible way to get __dirname
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
// node-gyp-build smartly resolves prebuilds as well as local builds.
|
|
9
|
+
const nodeGypBuild = require("node-gyp-build");
|
|
10
|
+
export default nodeGypBuild(join(__dirname, ".."));
|
package/js/variants.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type GlassMaterialVariant = number;
|
|
2
|
+
|
|
3
|
+
export const GlassMaterialVariant = {
|
|
4
|
+
regular: 0,
|
|
5
|
+
clear: 1,
|
|
6
|
+
dock: 2,
|
|
7
|
+
appIcons: 3,
|
|
8
|
+
widgets: 4,
|
|
9
|
+
text: 5,
|
|
10
|
+
avplayer: 6,
|
|
11
|
+
facetime: 7,
|
|
12
|
+
controlCenter: 8,
|
|
13
|
+
notificationCenter: 9,
|
|
14
|
+
monogram: 10,
|
|
15
|
+
bubbles: 11,
|
|
16
|
+
identity: 12,
|
|
17
|
+
focusBorder: 13,
|
|
18
|
+
focusPlatter: 14,
|
|
19
|
+
keyboard: 15,
|
|
20
|
+
sidebar: 16,
|
|
21
|
+
abuttedSidebar: 17,
|
|
22
|
+
inspector: 18,
|
|
23
|
+
control: 19,
|
|
24
|
+
loupe: 20,
|
|
25
|
+
slider: 21,
|
|
26
|
+
camera: 22,
|
|
27
|
+
cartouchePopover: 23,
|
|
28
|
+
} as const;
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zakim24/electron-liquid-glass",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "macOS glass / vibrancy wrapper for Electron BrowserWindow",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Meridius-Labs/electron-liquid-glass.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/Meridius-Labs/electron-liquid-glass/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/Meridius-Labs/electron-liquid-glass#readme",
|
|
13
|
+
"author": "Meridius Labs",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/index.d.cts",
|
|
27
|
+
"default": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"js",
|
|
34
|
+
"src",
|
|
35
|
+
"build/Release/*.node",
|
|
36
|
+
"prebuilds/**/*",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"build:native": "prebuildify --napi --strip --tag-armv --arch=arm64 && prebuildify --napi --strip --arch=x64",
|
|
43
|
+
"build:all": "bun run build:native && bun run build",
|
|
44
|
+
"clean": "rimraf build prebuilds dist",
|
|
45
|
+
"prepack": "bun run build:all",
|
|
46
|
+
"dev": "bun run build:all && run-p watch-examples start",
|
|
47
|
+
"watch-examples": "tsc -w --project tsconfig.examples.json",
|
|
48
|
+
"start": "bun x electronmon ./dist-examples/electron.js",
|
|
49
|
+
"version:patch": "npm version patch",
|
|
50
|
+
"version:minor": "npm version minor",
|
|
51
|
+
"version:major": "npm version major",
|
|
52
|
+
"version:prerelease": "npm version prerelease",
|
|
53
|
+
"release": "bun run build:all && npm publish",
|
|
54
|
+
"release:dry": "bun run build:all && npm publish --dry-run"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"electron",
|
|
58
|
+
"macos",
|
|
59
|
+
"vibrancy",
|
|
60
|
+
"glass",
|
|
61
|
+
"nsvisualeffectview"
|
|
62
|
+
],
|
|
63
|
+
"gypfile": false,
|
|
64
|
+
"os": [
|
|
65
|
+
"darwin"
|
|
66
|
+
],
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=18"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"bindings": "^1.5.0",
|
|
72
|
+
"node-addon-api": "^8.4.0"
|
|
73
|
+
},
|
|
74
|
+
"optionalDependencies": {
|
|
75
|
+
"node-gyp-build": "^4"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@3c1u/bun-run-all": "^0.1.2",
|
|
79
|
+
"@types/node": "^20",
|
|
80
|
+
"electron": "^38",
|
|
81
|
+
"node-gyp": "^11.2.0",
|
|
82
|
+
"prebuildify": "^5",
|
|
83
|
+
"rimraf": "^5",
|
|
84
|
+
"tsdown": "^0.12.8",
|
|
85
|
+
"typescript": "^5"
|
|
86
|
+
},
|
|
87
|
+
"packageManager": "bun@1.2.23"
|
|
88
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#include "../include/Common.h"
|
|
2
|
+
#include <napi.h>
|
|
3
|
+
#import <objc/runtime.h>
|
|
4
|
+
#import <objc/message.h>
|
|
5
|
+
#include <string>
|
|
6
|
+
#include <cctype>
|
|
7
|
+
|
|
8
|
+
#ifdef PLATFORM_OSX
|
|
9
|
+
#import <AppKit/AppKit.h>
|
|
10
|
+
|
|
11
|
+
// Simple registry so JS can still address a view by numeric id.
|
|
12
|
+
static std::map<int, NSView *> g_glassViews;
|
|
13
|
+
static int g_nextViewId = 0;
|
|
14
|
+
|
|
15
|
+
// Keys for objc-associated views on a container
|
|
16
|
+
static const void *kGlassEffectKey = &kGlassEffectKey;
|
|
17
|
+
static const void *kBackgroundViewKey = &kBackgroundViewKey;
|
|
18
|
+
|
|
19
|
+
// Utility: convert #RRGGBB or #RRGGBBAA to NSColor* (sRGB)
|
|
20
|
+
static NSColor* ColorFromHexNSString(NSString* hex)
|
|
21
|
+
{
|
|
22
|
+
NSString* cleaned = [[hex stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString];
|
|
23
|
+
if ([cleaned hasPrefix:@"#"]) cleaned = [cleaned substringFromIndex:1];
|
|
24
|
+
if (cleaned.length != 6 && cleaned.length != 8) return nil;
|
|
25
|
+
|
|
26
|
+
unsigned int rgba = 0;
|
|
27
|
+
NSScanner* scanner = [NSScanner scannerWithString:cleaned];
|
|
28
|
+
if (![scanner scanHexInt:&rgba]) return nil;
|
|
29
|
+
|
|
30
|
+
CGFloat r,g,b,a;
|
|
31
|
+
if (cleaned.length == 6) {
|
|
32
|
+
r = ((rgba & 0xFF0000) >> 16) / 255.0;
|
|
33
|
+
g = ((rgba & 0x00FF00) >> 8) / 255.0;
|
|
34
|
+
b = (rgba & 0x0000FF) / 255.0;
|
|
35
|
+
a = 1.0;
|
|
36
|
+
} else {
|
|
37
|
+
r = ((rgba & 0xFF000000) >> 24) / 255.0;
|
|
38
|
+
g = ((rgba & 0x00FF0000) >> 16) / 255.0;
|
|
39
|
+
b = ((rgba & 0x0000FF00) >> 8) / 255.0;
|
|
40
|
+
a = (rgba & 0x000000FF) / 255.0;
|
|
41
|
+
}
|
|
42
|
+
return [NSColor colorWithRed:r green:g blue:b alpha:a];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#define RUN_ON_MAIN(block) \
|
|
46
|
+
if ([NSThread isMainThread]) { \
|
|
47
|
+
block(); \
|
|
48
|
+
} else { \
|
|
49
|
+
dispatch_sync(dispatch_get_main_queue(), block); \
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/*!
|
|
53
|
+
* AddGlassEffectView
|
|
54
|
+
* -----------------
|
|
55
|
+
* Creates an `NSGlassEffectView` (private) and inserts it behind the contentView
|
|
56
|
+
* of the supplied Electron window. The handle received from JavaScript is the
|
|
57
|
+
* pointer to the Cocoa `NSView` that backs the BrowserWindow. The view is
|
|
58
|
+
* retained in a small registry so that we can manipulate or remove it later if
|
|
59
|
+
* required. The function returns an integer identifier that can be used from
|
|
60
|
+
* JavaScript.
|
|
61
|
+
*
|
|
62
|
+
* Returns β1 on error.
|
|
63
|
+
*/
|
|
64
|
+
extern "C" int AddGlassEffectView(unsigned char *buffer, bool opaque) {
|
|
65
|
+
if (!buffer) {
|
|
66
|
+
return -1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
__block int resultId = -1;
|
|
70
|
+
|
|
71
|
+
RUN_ON_MAIN(^{
|
|
72
|
+
NSView *rootView = *reinterpret_cast<NSView **>(buffer);
|
|
73
|
+
if (!rootView) return;
|
|
74
|
+
|
|
75
|
+
// Find the proper container - avoid NSThemeFrame
|
|
76
|
+
NSView *container = rootView;
|
|
77
|
+
|
|
78
|
+
// Remove previous glass and background views (if any)
|
|
79
|
+
NSView *oldGlass = objc_getAssociatedObject(container, kGlassEffectKey);
|
|
80
|
+
if (oldGlass) [oldGlass removeFromSuperview];
|
|
81
|
+
|
|
82
|
+
NSView *oldBackground = objc_getAssociatedObject(container, kBackgroundViewKey);
|
|
83
|
+
if (oldBackground) [oldBackground removeFromSuperview];
|
|
84
|
+
|
|
85
|
+
NSRect bounds = container.bounds;
|
|
86
|
+
|
|
87
|
+
NSBox *backgroundView = nil;
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
NSView *glass = nil;
|
|
91
|
+
Class glassCls = NSClassFromString(@"NSGlassEffectView");
|
|
92
|
+
if (glassCls) {
|
|
93
|
+
/**
|
|
94
|
+
* GLASS VIEW
|
|
95
|
+
*/
|
|
96
|
+
glass = [[glassCls alloc] initWithFrame:bounds];
|
|
97
|
+
|
|
98
|
+
if (opaque) {
|
|
99
|
+
// Create a background view behind the glass view using NSBox for proper background color
|
|
100
|
+
backgroundView = [[NSBox alloc] initWithFrame:bounds];
|
|
101
|
+
backgroundView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
102
|
+
backgroundView.boxType = NSBoxCustom;
|
|
103
|
+
backgroundView.borderType = NSNoBorder;
|
|
104
|
+
backgroundView.fillColor = [NSColor windowBackgroundColor];
|
|
105
|
+
backgroundView.wantsLayer = YES;
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
// Add the background view first (bottom layer)
|
|
109
|
+
[container addSubview:backgroundView positioned:NSWindowBelow relativeTo:nil];
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
/**
|
|
113
|
+
* FALLBACK VISUAL EFFECT VIEW
|
|
114
|
+
*/
|
|
115
|
+
NSVisualEffectView *visual = [[NSVisualEffectView alloc] initWithFrame:bounds];
|
|
116
|
+
visual.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
117
|
+
visual.blendingMode = NSVisualEffectBlendingModeBehindWindow;
|
|
118
|
+
visual.material = NSVisualEffectMaterialUnderWindowBackground;
|
|
119
|
+
visual.state = NSVisualEffectStateActive;
|
|
120
|
+
glass = visual;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Ensure autoresize if we created a private glass view too
|
|
124
|
+
glass.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
125
|
+
|
|
126
|
+
// Add the glass view (positioned relative to background view if opaque, or below everything if not)
|
|
127
|
+
if (opaque && backgroundView) {
|
|
128
|
+
[container addSubview:glass positioned:NSWindowAbove relativeTo:backgroundView];
|
|
129
|
+
} else {
|
|
130
|
+
[container addSubview:glass positioned:NSWindowBelow relativeTo:nil];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Associate views with the container for cleanup
|
|
134
|
+
objc_setAssociatedObject(container, kGlassEffectKey, glass, OBJC_ASSOCIATION_RETAIN);
|
|
135
|
+
if (backgroundView) {
|
|
136
|
+
objc_setAssociatedObject(container, kBackgroundViewKey, backgroundView, OBJC_ASSOCIATION_RETAIN);
|
|
137
|
+
} else {
|
|
138
|
+
objc_setAssociatedObject(container, kBackgroundViewKey, nil, OBJC_ASSOCIATION_ASSIGN);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
int id = g_nextViewId++;
|
|
144
|
+
g_glassViews[id] = glass;
|
|
145
|
+
resultId = id;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return resultId;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Configure glass view by id
|
|
152
|
+
extern "C" void ConfigureGlassView(int viewId, double cornerRadius, const char* tintHex) {
|
|
153
|
+
RUN_ON_MAIN(^{
|
|
154
|
+
auto it = g_glassViews.find(viewId);
|
|
155
|
+
if (it == g_glassViews.end()) return;
|
|
156
|
+
NSView* glass = it->second;
|
|
157
|
+
|
|
158
|
+
// Corner radius via CALayer
|
|
159
|
+
glass.wantsLayer = YES;
|
|
160
|
+
glass.layer.cornerRadius = cornerRadius;
|
|
161
|
+
glass.layer.masksToBounds = YES;
|
|
162
|
+
|
|
163
|
+
// corner radius for the background view
|
|
164
|
+
NSView* container = glass.superview;
|
|
165
|
+
NSView* backgroundView = objc_getAssociatedObject(container, kBackgroundViewKey);
|
|
166
|
+
if (backgroundView) {
|
|
167
|
+
backgroundView.wantsLayer = YES;
|
|
168
|
+
backgroundView.layer.cornerRadius = cornerRadius;
|
|
169
|
+
backgroundView.layer.masksToBounds = YES;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (tintHex && strlen(tintHex) > 0) {
|
|
173
|
+
NSString* hex = [NSString stringWithUTF8String:tintHex];
|
|
174
|
+
NSColor* c = ColorFromHexNSString(hex);
|
|
175
|
+
if (c && [glass respondsToSelector:@selector(setTintColor:)]) {
|
|
176
|
+
[(id)glass setTintColor:c];
|
|
177
|
+
} else if (c) {
|
|
178
|
+
glass.layer.backgroundColor = c.CGColor;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// -----------------------------------------------------------------------------
|
|
185
|
+
// Dynamically set private properties on a previously created glass view
|
|
186
|
+
// -----------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
// Helper that converts a C-string key (e.g. "variant") into the Objective-C
|
|
189
|
+
// selector for its private setter (e.g. set_variant:). It automatically adds
|
|
190
|
+
// the leading underscore when missing.
|
|
191
|
+
static SEL SetterFromKey(const std::string &key, bool privateVariant) {
|
|
192
|
+
std::string name;
|
|
193
|
+
if (privateVariant) {
|
|
194
|
+
// ensure leading underscore
|
|
195
|
+
if (!key.empty() && key.front() != '_')
|
|
196
|
+
name = "_" + key;
|
|
197
|
+
else
|
|
198
|
+
name = key;
|
|
199
|
+
name = "set" + name;
|
|
200
|
+
} else {
|
|
201
|
+
// camel-case public variant: set + CapitalizedFirst + rest
|
|
202
|
+
if (key.empty()) return nil;
|
|
203
|
+
name = "set";
|
|
204
|
+
name += toupper(key[0]);
|
|
205
|
+
name += key.substr(1);
|
|
206
|
+
}
|
|
207
|
+
name += ":";
|
|
208
|
+
return sel_registerName(name.c_str());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
static SEL ResolveSetter(id obj, const char* cKey) {
|
|
212
|
+
if (!cKey) return nil;
|
|
213
|
+
std::string key(cKey);
|
|
214
|
+
if (key.empty()) return nil;
|
|
215
|
+
// Try private style first (set_<key>:)
|
|
216
|
+
SEL sel = SetterFromKey(key, true);
|
|
217
|
+
if ([obj respondsToSelector:sel]) return sel;
|
|
218
|
+
// Then try public style (setKey:)
|
|
219
|
+
sel = SetterFromKey(key, false);
|
|
220
|
+
if ([obj respondsToSelector:sel]) return sel;
|
|
221
|
+
return nil;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
extern "C" void SetGlassViewIntProperty(int viewId, const char* key, long long value) {
|
|
225
|
+
#ifdef PLATFORM_OSX
|
|
226
|
+
RUN_ON_MAIN(^{
|
|
227
|
+
auto it = g_glassViews.find(viewId);
|
|
228
|
+
if (it == g_glassViews.end()) return;
|
|
229
|
+
NSView* glass = it->second;
|
|
230
|
+
|
|
231
|
+
SEL sel = ResolveSetter(glass, key);
|
|
232
|
+
if (!sel) return;
|
|
233
|
+
if ([glass respondsToSelector:sel]) {
|
|
234
|
+
((void (*)(id, SEL, long long))objc_msgSend)(glass, sel, value);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
#endif
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
extern "C" void SetGlassViewStringProperty(int viewId, const char* key, const char* value) {
|
|
241
|
+
#ifdef PLATFORM_OSX
|
|
242
|
+
RUN_ON_MAIN(^{
|
|
243
|
+
auto it = g_glassViews.find(viewId);
|
|
244
|
+
if (it == g_glassViews.end()) return;
|
|
245
|
+
NSView* glass = it->second;
|
|
246
|
+
|
|
247
|
+
SEL sel = ResolveSetter(glass, key);
|
|
248
|
+
if (!sel) return;
|
|
249
|
+
if ([glass respondsToSelector:sel]) {
|
|
250
|
+
NSString* val = value ? [NSString stringWithUTF8String:value] : @"";
|
|
251
|
+
((void (*)(id, SEL, id))objc_msgSend)(glass, sel, val);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
#endif
|
|
255
|
+
}
|
|
256
|
+
#endif // PLATFORM_OSX
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#include <napi.h>
|
|
2
|
+
#include <string>
|
|
3
|
+
|
|
4
|
+
#ifdef __APPLE__
|
|
5
|
+
extern "C" int AddGlassEffectView(unsigned char *buffer, bool opaque);
|
|
6
|
+
extern "C" void ConfigureGlassView(int viewId, double cornerRadius, const char *tintHex);
|
|
7
|
+
extern "C" void SetGlassViewIntProperty(int viewId, const char *key, long long value);
|
|
8
|
+
extern "C" void SetGlassViewStringProperty(int viewId, const char *key, const char *value);
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
// Create a class that will be exposed to JavaScript
|
|
12
|
+
class LiquidGlassNative : public Napi::ObjectWrap<LiquidGlassNative>
|
|
13
|
+
{
|
|
14
|
+
public:
|
|
15
|
+
// This static method defines the class for JavaScript
|
|
16
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports)
|
|
17
|
+
{
|
|
18
|
+
// Define the JavaScript class with method(s)
|
|
19
|
+
Napi::Function func = DefineClass(env, "LiquidGlassNative", {InstanceMethod("addView", &LiquidGlassNative::AddView), InstanceMethod("setVariant", &LiquidGlassNative::SetVariant), InstanceMethod("setScrimState", &LiquidGlassNative::SetScrimState), InstanceMethod("setSubduedState", &LiquidGlassNative::SetSubduedState)});
|
|
20
|
+
|
|
21
|
+
// Create a persistent reference to the constructor
|
|
22
|
+
Napi::FunctionReference *constructor = new Napi::FunctionReference();
|
|
23
|
+
*constructor = Napi::Persistent(func);
|
|
24
|
+
env.SetInstanceData(constructor);
|
|
25
|
+
|
|
26
|
+
// Set the constructor on the exports object
|
|
27
|
+
exports.Set("LiquidGlassNative", func);
|
|
28
|
+
return exports;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Constructor
|
|
32
|
+
LiquidGlassNative(const Napi::CallbackInfo &info)
|
|
33
|
+
: Napi::ObjectWrap<LiquidGlassNative>(info) {}
|
|
34
|
+
|
|
35
|
+
private:
|
|
36
|
+
// New AddView method
|
|
37
|
+
Napi::Value AddView(const Napi::CallbackInfo &info)
|
|
38
|
+
{
|
|
39
|
+
Napi::Env env = info.Env();
|
|
40
|
+
|
|
41
|
+
if (info.Length() < 1 || !info[0].IsBuffer())
|
|
42
|
+
{
|
|
43
|
+
Napi::TypeError::New(env, "Expected first argument to be a Buffer returned by getNativeWindowHandle()").ThrowAsJavaScriptException();
|
|
44
|
+
return env.Null();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
double radius = 0.0;
|
|
48
|
+
std::string tint;
|
|
49
|
+
bool opaque = false;
|
|
50
|
+
if (info.Length() >= 2 && info[1].IsObject())
|
|
51
|
+
{
|
|
52
|
+
auto obj = info[1].As<Napi::Object>();
|
|
53
|
+
if (obj.Has("cornerRadius") && obj.Get("cornerRadius").IsNumber())
|
|
54
|
+
{
|
|
55
|
+
radius = obj.Get("cornerRadius").As<Napi::Number>().DoubleValue();
|
|
56
|
+
}
|
|
57
|
+
if (obj.Has("tintColor") && obj.Get("tintColor").IsString())
|
|
58
|
+
{
|
|
59
|
+
tint = obj.Get("tintColor").As<Napi::String>().Utf8Value();
|
|
60
|
+
}
|
|
61
|
+
if (obj.Has("opaque") && obj.Get("opaque").IsBoolean())
|
|
62
|
+
{
|
|
63
|
+
opaque = obj.Get("opaque").As<Napi::Boolean>().Value();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
auto buffer = info[0].As<Napi::Buffer<unsigned char>>();
|
|
68
|
+
|
|
69
|
+
#ifdef __APPLE__
|
|
70
|
+
int viewId = AddGlassEffectView(buffer.Data(), opaque);
|
|
71
|
+
if (viewId >= 0)
|
|
72
|
+
{
|
|
73
|
+
ConfigureGlassView(viewId, radius, tint.c_str());
|
|
74
|
+
}
|
|
75
|
+
return Napi::Number::New(env, viewId);
|
|
76
|
+
#else
|
|
77
|
+
// Not supported on this platform yet
|
|
78
|
+
return Napi::Number::New(env, -1);
|
|
79
|
+
#endif
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Napi::Value SetVariant(const Napi::CallbackInfo &info)
|
|
83
|
+
{
|
|
84
|
+
Napi::Env env = info.Env();
|
|
85
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber())
|
|
86
|
+
{
|
|
87
|
+
Napi::TypeError::New(env, "Expected (id:number, variant:number)").ThrowAsJavaScriptException();
|
|
88
|
+
return env.Null();
|
|
89
|
+
}
|
|
90
|
+
int id = info[0].As<Napi::Number>().Int32Value();
|
|
91
|
+
long long variant = info[1].As<Napi::Number>().Int64Value();
|
|
92
|
+
ApplyIntProp(id, "variant", variant);
|
|
93
|
+
return env.Undefined();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
Napi::Value SetScrimState(const Napi::CallbackInfo &info)
|
|
97
|
+
{
|
|
98
|
+
Napi::Env env = info.Env();
|
|
99
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber())
|
|
100
|
+
{
|
|
101
|
+
Napi::TypeError::New(env, "Expected (id:number, scrim:number)").ThrowAsJavaScriptException();
|
|
102
|
+
return env.Null();
|
|
103
|
+
}
|
|
104
|
+
int id = info[0].As<Napi::Number>().Int32Value();
|
|
105
|
+
long long scrim = info[1].As<Napi::Number>().Int64Value();
|
|
106
|
+
ApplyIntProp(id, "scrimState", scrim);
|
|
107
|
+
return env.Undefined();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Napi::Value SetSubduedState(const Napi::CallbackInfo &info)
|
|
111
|
+
{
|
|
112
|
+
Napi::Env env = info.Env();
|
|
113
|
+
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber())
|
|
114
|
+
{
|
|
115
|
+
Napi::TypeError::New(env, "Expected (id:number, subdued:number)").ThrowAsJavaScriptException();
|
|
116
|
+
return env.Null();
|
|
117
|
+
}
|
|
118
|
+
int id = info[0].As<Napi::Number>().Int32Value();
|
|
119
|
+
long long subd = info[1].As<Napi::Number>().Int64Value();
|
|
120
|
+
ApplyIntProp(id, "subduedState", subd);
|
|
121
|
+
return env.Undefined();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static void ApplyIntProp(int id, const char *key, long long v)
|
|
125
|
+
{
|
|
126
|
+
#ifdef __APPLE__
|
|
127
|
+
SetGlassViewIntProperty(id, key, v);
|
|
128
|
+
#endif
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Initialize the addon
|
|
133
|
+
Napi::Object Init(Napi::Env env, Napi::Object exports)
|
|
134
|
+
{
|
|
135
|
+
return LiquidGlassNative::Init(env, exports);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Register the initialization function
|
|
139
|
+
NODE_API_MODULE(liquidglass, Init)
|