feat: Add Windows 11 Acrylic window support (#11481)

Co-authored-by: Eugene <inbox@null.page>
This commit is contained in:
KaniPan
2026-09-24 21:10:31 +02:00
committed by GitHub
co-authored by Eugene
parent 062305a129
commit 6193527bae
10 changed files with 100 additions and 69 deletions
+2 -2
View File
@@ -219,7 +219,7 @@ export class Application {
return
}
this.tray?.destroy()
this.tray = null
this.tray = undefined
}
hasWindows (): boolean {
@@ -374,7 +374,7 @@ export class Application {
]
if (process.env.TABBY_DEV) {
template[2].submenu['unshift']({ role: 'reload' })
template[2].submenu!['unshift']({ role: 'reload' })
}
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
+1 -1
View File
@@ -91,7 +91,7 @@ if (!app.requestSingleInstanceLock()) {
app.on('ready', async () => {
if (process.platform === 'darwin') {
app.dock.setMenu(Menu.buildFromTemplate([
app.dock?.setMenu(Menu.buildFromTemplate([
{
label: 'New window',
click () {
+2 -2
View File
@@ -44,11 +44,11 @@ class PTYDataQueue {
return
}
const buffersToSend = []
const buffersToSend: Buffer[] = []
let totalLength = 0
while (totalLength < this.maxChunk && this.buffers.length) {
totalLength += this.buffers[0].length
buffersToSend.push(this.buffers.shift())
buffersToSend.push(this.buffers.shift()!)
}
if (buffersToSend.length === 0) {
+76 -45
View File
@@ -4,7 +4,6 @@ import { Subject, Observable, debounceTime } from 'rxjs'
import { BrowserWindow, app, ipcMain, Rectangle, Menu, screen, BrowserWindowConstructorOptions, TouchBar, nativeImage, WebContents, nativeTheme } from 'electron'
import ElectronConfig = require('electron-config')
import { enable as enableRemote } from '@electron/remote/main'
import * as os from 'os'
import * as path from 'path'
import macOSRelease from 'macos-release'
import { compare as compareVersions } from 'compare-versions'
@@ -12,21 +11,20 @@ import { compare as compareVersions } from 'compare-versions'
import type { Application } from './app'
import { parseArgs } from './cli'
import { parseTabbyURL, isTabbyURL } from './urlHandler'
import { isWindowsBuild } from './windows'
let DwmEnableBlurBehindWindow: any = null
if (process.platform === 'win32') {
DwmEnableBlurBehindWindow = require('@tabby-gang/windows-blurbehind').DwmEnableBlurBehindWindow
}
export interface WindowOptions {
hidden?: boolean
}
abstract class GlasstronWindow extends BrowserWindow {
blurType: string
abstract setBlur (_: boolean)
type GlasstronWindow = BrowserWindow & {
blurType?: string | null
setBlur?: (_: boolean) => void
}
const isWindows11 = isWindowsBuild(22621)
const macOSVibrancyType: any = process.platform === 'darwin' ? compareVersions(macOSRelease().version || '0.0', '10.14', '>=') ? 'fullscreen-ui' : 'dark' : null
const activityIcon = nativeImage.createFromPath(`${app.getAppPath()}/assets/activity.png`)
@@ -51,8 +49,8 @@ export class Window {
get closed$ (): Observable<void> { return this.closed }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
constructor (private application: Application, private configStore: any, options?: WindowOptions) {
options = options ?? {}
constructor (private application: Application, private configStore: any, _options?: WindowOptions) {
const options = _options ?? {}
this.windowConfig = new ElectronConfig({ name: 'window' })
this.windowBounds = this.windowConfig.get('windowBoundaries')
@@ -85,8 +83,8 @@ export class Window {
const [left2, top2, right2, bottom2] = [closestDisplay.bounds.x, closestDisplay.bounds.y, closestDisplay.bounds.x + closestDisplay.bounds.width, closestDisplay.bounds.y + closestDisplay.bounds.height]
if ((left2 > right1 || right2 < left1 || top2 > bottom1 || bottom2 < top1) && !maximized) {
bwOptions.x = closestDisplay.bounds.width / 2 - bwOptions.width / 2
bwOptions.y = closestDisplay.bounds.height / 2 - bwOptions.height / 2
bwOptions.x = closestDisplay.bounds.width / 2 - bwOptions.width! / 2
bwOptions.y = closestDisplay.bounds.height / 2 - bwOptions.height! / 2
}
}
@@ -105,17 +103,17 @@ export class Window {
bwOptions.visualEffectState = 'active'
}
if (process.platform === 'darwin') {
if (process.platform === 'darwin' || isWindows11) {
this.window = new BrowserWindow(bwOptions) as GlasstronWindow
} else {
this.window = new glasstron.BrowserWindow(bwOptions)
this.window = new glasstron.BrowserWindow(bwOptions) as GlasstronWindow
}
this.webContents = this.window.webContents
this.window.webContents.once('did-finish-load', () => {
if (process.platform === 'darwin') {
this.window.setVibrancy(macOSVibrancyType)
this.window!.setVibrancy(macOSVibrancyType)
} else if (process.platform === 'win32' && this.configStore.appearance?.vibrancy) {
this.setVibrancy(true)
}
@@ -124,12 +122,12 @@ export class Window {
if (!options.hidden) {
if (maximized) {
this.window.maximize()
this.window!.maximize()
} else {
this.window.show()
this.window!.show()
}
this.window.focus()
this.window.moveTop()
this.window!.focus()
this.window!.moveTop()
application.focus()
}
})
@@ -139,7 +137,7 @@ export class Window {
(this.configStore.appearance?.dock ?? 'off') !== 'off' &&
this.configStore.appearance?.dockHideOnBlur &&
!BrowserWindow.getFocusedWindow() &&
this.window.isEnabled()
this.window?.isEnabled()
) {
this.hide()
}
@@ -172,7 +170,7 @@ export class Window {
this.ready = new Promise(resolve => {
const listener = event => {
if (event.sender === this.window.webContents) {
if (event.sender === this.window?.webContents) {
ipcMain.removeListener('app:ready', listener as any)
resolve()
}
@@ -183,28 +181,36 @@ export class Window {
makeMain (): void {
this.isMainWindow = true
this.window.webContents.send('host:became-main-window')
this.window?.webContents.send('host:became-main-window')
}
setVibrancy (enabled: boolean, type?: string, userRequested?: boolean): void {
setVibrancy(enabled: boolean, type?: string, userRequested?: boolean): void {
if (!this.window) {
return
}
if (userRequested ?? true) {
this.lastVibrancy = { enabled, type }
}
if (process.platform === 'win32') {
if (parseFloat(os.release()) >= 10) {
this.window.blurType = enabled ? type === 'fluent' ? 'acrylic' : 'blurbehind' : null
if (isWindows11) {
try {
this.window.setBlur(enabled)
this.window.setBackgroundMaterial(enabled ? 'acrylic' : 'none')
this.isFluentVibrancy = enabled
} catch (error) {
console.error('Failed to set window acrylic', error)
}
} else {
try {
this.window.blurType = enabled ? type === 'fluent' ? 'acrylic' : 'blurbehind' : null
this.window.setBlur?.(enabled)
this.isFluentVibrancy = enabled && type === 'fluent'
} catch (error) {
console.error('Failed to set window blur', error)
}
} else {
DwmEnableBlurBehindWindow(this.window.getNativeWindowHandle(), enabled)
}
} else if (process.platform === 'linux') {
this.window.setBackgroundColor(enabled ? '#00000000' : '#131d27')
this.window.setBlur(enabled)
this.window.setBlur?.(enabled)
} else {
this.window.setVibrancy(enabled ? macOSVibrancyType : null)
}
@@ -223,7 +229,7 @@ export class Window {
}
focus (): void {
this.window.focus()
this.window?.focus()
}
send (event: string, ...args: any[]): void {
@@ -242,11 +248,11 @@ export class Window {
}
isFocused (): boolean {
return this.window.isFocused()
return this.window!.isFocused()
}
isVisible (): boolean {
return this.window.isVisible()
return this.window!.isVisible()
}
isDockedOnTop (): boolean {
@@ -254,6 +260,9 @@ export class Window {
}
async hide (): Promise<void> {
if (!this.window) {
return
}
if (process.platform === 'darwin') {
// Lose focus
Menu.sendActionToFirstResponder('hide:')
@@ -270,12 +279,17 @@ export class Window {
}
async show (): Promise<void> {
await this.enableDockedWindowStyles(this.isDockedOnTop())
if (!this.window) {
return
} await this.enableDockedWindowStyles(this.isDockedOnTop())
this.window.show()
this.window.focus()
}
async present (): Promise<void> {
if (!this.window) {
return
}
await this.show()
this.window.moveTop()
}
@@ -290,10 +304,13 @@ export class Window {
}
private async enableDockedWindowStyles (enabled: boolean) {
if (!this.window) {
return
}
if (process.platform === 'darwin') {
if (enabled) {
if (!this.dockHidden) {
app.dock.hide()
app.dock?.hide()
this.dockHidden = true
}
this.window.setAlwaysOnTop(true, 'screen-saver', 1)
@@ -305,7 +322,7 @@ export class Window {
}
} else {
if (this.dockHidden) {
await app.dock.show()
await app.dock?.show()
this.dockHidden = false
}
if (this.window.isAlwaysOnTop()) {
@@ -322,6 +339,9 @@ export class Window {
}
private setupWindowManagement () {
if (!this.window) {
return
}
this.window.on('show', () => {
this.visible.next(true)
this.send('host:window-shown')
@@ -332,7 +352,7 @@ export class Window {
})
const moveSubscription = new Observable<void>(observer => {
this.window.on('move', () => observer.next())
this.window?.on('move', () => observer.next())
}).pipe(debounceTime(250)).subscribe(() => {
this.send('host:window-moved')
})
@@ -354,7 +374,7 @@ export class Window {
return
}
this.windowConfig.set('windowBoundaries', this.windowBounds)
this.windowConfig.set('maximized', this.window.isMaximized())
this.windowConfig.set('maximized', this.window!.isMaximized())
})
this.window.on('closed', () => {
@@ -362,20 +382,31 @@ export class Window {
})
this.window.on('resize', () => {
if (!this.window.isMaximized()) {
this.windowBounds = this.window.getBounds()
if (!this.window?.isMaximized()) {
this.windowBounds = this.window?.getBounds()
}
})
this.window.on('move', () => {
if (!this.window.isMaximized()) {
this.windowBounds = this.window.getBounds()
if (!this.window?.isMaximized()) {
this.windowBounds = this.window?.getBounds()
}
})
this.window.on('focus', () => {
this.window.flashFrame(false)
this.window?.flashFrame(false)
this.send('host:window-focused')
// Re-apply acrylic on Win11 when window gains focus
if (isWindows11 && this.lastVibrancy?.enabled) {
this.window?.setBackgroundMaterial('acrylic')
}
})
this.window.on('blur', () => {
// Re-apply acrylic on Win11 when window loses focus
if (isWindows11 && this.lastVibrancy?.enabled) {
this.window?.setBackgroundMaterial('acrylic')
}
})
this.on('ready', () => {
@@ -433,7 +464,7 @@ export class Window {
this.on('window-close', () => {
this.closing = true
this.window.close()
this.window?.close()
})
this.on('window-set-touch-bar', (_, segments, selectedIndex) => {
@@ -462,7 +493,7 @@ export class Window {
clearTimeout(moveEndedTimeout)
}
moveEndedTimeout = setTimeout(() => {
this.setVibrancy(this.lastVibrancy.enabled, this.lastVibrancy.type)
this.setVibrancy(this.lastVibrancy!.enabled, this.lastVibrancy!.type)
}, 50)
}
this.window.on('move', onBoundsChange)
@@ -526,7 +557,7 @@ export class Window {
}
private destroy () {
this.window = null
this.window = undefined
this.closed.next()
this.visible.complete()
this.closed.complete()
+10
View File
@@ -0,0 +1,10 @@
import * as os from 'node:os'
export function getWindows10Build(): number | undefined {
return process.platform === 'win32' && parseFloat(os.release()) >= 10 ? parseInt(os.release().split('.')[2], 10) : undefined
}
export function isWindowsBuild (build: number): boolean {
const b = getWindows10Build()
return b !== undefined && b >= build
}
-1
View File
@@ -37,7 +37,6 @@
"yargs": "^18"
},
"optionalDependencies": {
"@tabby-gang/windows-blurbehind": "^3",
"@tabby-gang/windows-process-tree": "^0.6.1",
"macos-native-processlist": "^2",
"patch-package": "^8",
+8 -5
View File
@@ -69,7 +69,7 @@ export type ProgressCallback = (current: number, total: number) => void
export function initModuleLookup (userPluginsPath: string): void {
global['module'].paths.map((x: string) => nodeModule.globalPaths.push(normalizePath(x)))
const paths = []
const paths: string[] = []
paths.unshift(path.join(userPluginsPath, 'node_modules'))
paths.unshift(path.join(remote.app.getAppPath(), 'node_modules'))
@@ -108,7 +108,7 @@ async function getCandidateLocationsInPluginDir (pluginDir: any): Promise<{ plug
})
}
const promises = []
const promises: Promise<void>[] = []
for (const packageName of pluginNames) {
if ((packageName.startsWith(PLUGIN_PREFIX) || packageName.startsWith(LEGACY_PLUGIN_PREFIX)) && !PLUGIN_BLACKLIST.includes(packageName)) {
@@ -128,10 +128,10 @@ async function getCandidateLocationsInPluginDir (pluginDir: any): Promise<{ plug
return candidateLocations
}
async function getPluginCandidateLocation (paths: any): Promise<{ pluginDir: string, packageName: string }[]> {
async function getPluginCandidateLocation (paths: string[]): Promise<{ pluginDir: string, packageName: string }[]> {
const candidateLocationsPromises: Promise<{ pluginDir: string, packageName: string }[]>[] = []
const processedPaths = []
const processedPaths: string[] = []
for (let pluginDir of paths) {
if (processedPaths.includes(pluginDir)) {
@@ -228,7 +228,7 @@ export async function findPlugins (): Promise<PluginInfo[]> {
export async function loadPlugins (foundPlugins: PluginInfo[], progress: ProgressCallback): Promise<any[]> {
const plugins: any[] = []
const pluginsPromises: Promise<any>[] = []
const pluginsPromises: Promise<void>[] = []
let index = 0
const setProgress = function () {
@@ -239,6 +239,9 @@ export async function loadPlugins (foundPlugins: PluginInfo[], progress: Progres
progress(0, 1)
for (const foundPlugin of foundPlugins) {
pluginsPromises.push(new Promise(x => {
if (!foundPlugin.path) {
return
}
console.info(`Loading ${foundPlugin.name}: ${nodeRequire.resolve(foundPlugin.path)}`)
try {
const packageModule = nodeRequire(foundPlugin.path)
+1
View File
@@ -15,6 +15,7 @@
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"lib": [
"dom",
"es2015",
-1
View File
@@ -52,7 +52,6 @@ const config = {
util: 'commonjs util',
'windows-swca': 'commonjs windows-swca',
'windows-native-registry': 'commonjs windows-native-registry',
'@tabby-gang/windows-blurbehind': 'commonjs @tabby-gang/windows-blurbehind',
'yargs/yargs': 'commonjs yargs/yargs',
},
plugins: [
-12
View File
@@ -346,13 +346,6 @@
"@sigstore/core" "^3.2.1"
"@sigstore/protobuf-specs" "^0.5.0"
"@tabby-gang/windows-blurbehind@^3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@tabby-gang/windows-blurbehind/-/windows-blurbehind-3.1.0.tgz#bd1462f2a970d195e277d48cceb8b2c0a20f09bd"
integrity sha512-wTvyNrBDNxD4yq1bXv7lvXRQujJYRQ2Ke3LJyE9yzY9e/L9/fHVIuprIgAMiLsnW+BKPERq0k27iC38WnxUBZQ==
dependencies:
"@types/node" "^10.12.18"
"@tabby-gang/windows-process-tree@^0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@tabby-gang/windows-process-tree/-/windows-process-tree-0.6.1.tgz#2a5a5cbbee16f611fc61d53cbadf930c8d3d20a8"
@@ -387,11 +380,6 @@
dependencies:
undici-types "~6.21.0"
"@types/node@^10.12.18":
version "10.17.60"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
"@types/semver@^7.3.6":
version "7.8.0"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.8.0.tgz#0bfe3ec51f5e9615bc317174cd5b88ea08b7fc2f"