diff --git a/native/binding.gyp b/native/binding.gyp index 1e224ca..2649e6b 100644 --- a/native/binding.gyp +++ b/native/binding.gyp @@ -15,6 +15,7 @@ "src/waveform.cpp", "src/vumeter.cpp", "src/lufsmeter.cpp", + "src/window_chrome.cpp", "src/dsp_utils.cpp" ], "include_dirs": [ @@ -55,7 +56,8 @@ "ole32.lib", "avrt.lib", "runtimeobject.lib", - "uuid.lib" + "uuid.lib", + "dwmapi.lib" ], "msvs_settings": { "VCCLCompilerTool": { diff --git a/native/src/main.cpp b/native/src/main.cpp index e96cc55..58ddf42 100644 --- a/native/src/main.cpp +++ b/native/src/main.cpp @@ -12,6 +12,7 @@ #include "waveform.h" #include "vumeter.h" #include "lufsmeter.h" +#include "window_chrome.h" // Global instances static Visualizer::Oscilloscope oscilloscope; @@ -741,6 +742,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) { RegisterMacOSCapture(env, exports); RegisterWindowsCapture(env, exports); RegisterLinuxCapture(env, exports); + RegisterWindowChrome(env, exports); return exports; } diff --git a/native/src/window_chrome.cpp b/native/src/window_chrome.cpp new file mode 100644 index 0000000..45ab208 --- /dev/null +++ b/native/src/window_chrome.cpp @@ -0,0 +1,96 @@ +#include "window_chrome.h" + +#ifdef _WIN32 + +#include +#include + +// These DWM attributes/values exist on Windows 11 (build 22000+) but may be +// missing from older SDK headers, so provide fallback definitions. +#ifndef DWMWA_NCRENDERING_POLICY +#define DWMWA_NCRENDERING_POLICY 2 +#endif +#ifndef DWMNCRP_DISABLED +#define DWMNCRP_DISABLED 1 +#endif +#ifndef DWMWA_WINDOW_CORNER_PREFERENCE +#define DWMWA_WINDOW_CORNER_PREFERENCE 33 +#endif +#ifndef DWMWA_BORDER_COLOR +#define DWMWA_BORDER_COLOR 34 +#endif +#ifndef DWMWCP_DONOTROUND +#define DWMWCP_DONOTROUND 1 +#endif +#ifndef DWMWA_COLOR_NONE +#define DWMWA_COLOR_NONE 0xFFFFFFFE +#endif + +namespace { + +Napi::Value ApplyFlatFrame(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + if (info.Length() < 1 || !info[0].IsBuffer()) { + Napi::TypeError::New(env, "Expected native window handle Buffer") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + + Napi::Buffer handle = info[0].As>(); + if (handle.Length() < sizeof(HWND)) { + Napi::TypeError::New(env, "Window handle Buffer is too small") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + + HWND hwnd = *reinterpret_cast(handle.Data()); + if (hwnd == nullptr || !IsWindow(hwnd)) { + return Napi::Boolean::New(env, false); + } + + // The window keeps WS_THICKFRAME so native Aero Snap and edge resize work, but + // on Windows 11 that style makes DWM paint a visible border and a drop shadow + // ("depth"). Disabling DWM non-client rendering removes that shadow/border, + // and squaring the corners + clearing the accent border color restores the old + // flat, borderless look. All four calls are no-ops/benign on Windows 10 and + // fail harmlessly on unsupported builds. + DWORD ncRendering = DWMNCRP_DISABLED; + DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &ncRendering, sizeof(ncRendering)); + + DWORD cornerPreference = DWMWCP_DONOTROUND; + DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(cornerPreference)); + + COLORREF borderColor = DWMWA_COLOR_NONE; + DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, &borderColor, sizeof(borderColor)); + + MARGINS margins = {0, 0, 0, 0}; + DwmExtendFrameIntoClientArea(hwnd, &margins); + + return Napi::Boolean::New(env, true); +} + +} // namespace + +void RegisterWindowChrome(Napi::Env env, Napi::Object exports) { + Napi::Object chrome = Napi::Object::New(env); + chrome.Set("applyFlatFrame", Napi::Function::New(env, ApplyFlatFrame)); + exports.Set("windowChrome", chrome); +} + +#else // !_WIN32 + +namespace { + +Napi::Value ApplyFlatFrameNoop(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(info.Env(), false); +} + +} // namespace + +void RegisterWindowChrome(Napi::Env env, Napi::Object exports) { + Napi::Object chrome = Napi::Object::New(env); + chrome.Set("applyFlatFrame", Napi::Function::New(env, ApplyFlatFrameNoop)); + exports.Set("windowChrome", chrome); +} + +#endif // _WIN32 diff --git a/native/src/window_chrome.h b/native/src/window_chrome.h new file mode 100644 index 0000000..a1553ee --- /dev/null +++ b/native/src/window_chrome.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +// Registers the `windowChrome` export. On Windows it exposes `applyFlatFrame`, +// which strips the visible native frame/shadow from a frameless WS_THICKFRAME +// window while preserving native Aero Snap and edge resize. On other platforms +// it registers a no-op so the JS surface stays uniform. +void RegisterWindowChrome(Napi::Env env, Napi::Object exports); diff --git a/src/main/index.ts b/src/main/index.ts index 11dd7c4..60cfb2c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -33,6 +33,7 @@ import { import { calculateResizedWindowBounds } from '../shared/windowResize' import { FileBackedProfileLibrary } from './profileLibrary' import { loadNativeWindowsMediaApi } from './nativeWindowsMedia' +import { loadNativeWindowChromeApi } from './nativeWindowChrome' import { NowPlayingManager } from './services/nowPlayingManager' import { AstraIntegrationService } from './services/astraIntegration' import { MacSpotifyProvider } from './services/macSpotifyProvider' @@ -41,6 +42,7 @@ import { checkForUpdates, resolveSafeReleaseUrl } from './services/updates' import { FileBackedThemeLibrary } from './themeLibrary' import { FileBackedWindowStateStore } from './windowStateStore' import type { NativeWindowsMediaAPI } from '../types/nativeWindowsMedia' +import type { NativeWindowChromeAPI } from '../types/nativeWindowChrome' let mainWindow: BrowserWindow | null = null let moveInterval: ReturnType | null = null @@ -74,6 +76,7 @@ let nowPlayingManager: NowPlayingManager | null = null let windowStateStore: FileBackedWindowStateStore | null = null let secretVault: SecretVault | null = null let nativeWindowsMediaApi: NativeWindowsMediaAPI | null | undefined +let nativeWindowChromeApi: NativeWindowChromeAPI | null | undefined const WINDOW_DEFAULTS = { width: 900, @@ -350,6 +353,35 @@ function getNativeWindowsMediaApi(): NativeWindowsMediaAPI | null { return nativeWindowsMediaApi } +function getNativeWindowChromeApi(): NativeWindowChromeAPI | null { + if (nativeWindowChromeApi !== undefined) { + return nativeWindowChromeApi + } + + nativeWindowChromeApi = loadNativeWindowChromeApi() + return nativeWindowChromeApi +} + +// Strips the visible native frame/shadow from a frameless WS_THICKFRAME window so +// it looks flat again, while keeping native Aero Snap and edge resize. Windows-only; +// a no-op on macOS/Linux. +function applyFlatFramelessChrome(window: BrowserWindow): void { + if (process.platform !== 'win32') { + return + } + + const api = getNativeWindowChromeApi() + if (!api) { + return + } + + try { + api.applyFlatFrame(window.getNativeWindowHandle()) + } catch { + // Frame styling is purely cosmetic; never block window creation on it. + } +} + function getNowPlayingManager(): NowPlayingManager { if (!nowPlayingManager) { nowPlayingManager = new NowPlayingManager({ @@ -1009,6 +1041,7 @@ function createMainWindow(): void { backgroundThrottling: false, }, }) + applyFlatFramelessChrome(mainWindow) syncMainWindowLogicalBounds(mainWindow) mainWindow.on('close', (event) => { @@ -1218,6 +1251,7 @@ function createScopePopoutWindow(kind: ScopeKind, rawBounds?: WindowBounds): Bro } const popoutWindow = new BrowserWindow(options) + applyFlatFramelessChrome(popoutWindow) setSettingsHeightForWindow(popoutWindow, 0) scopePopoutWindows.set(kind, popoutWindow) @@ -1385,6 +1419,7 @@ function createNowPlayingConfigWindow(): BrowserWindow { } nowPlayingConfigWindow = new BrowserWindow(options) + applyFlatFramelessChrome(nowPlayingConfigWindow) const configWindow = nowPlayingConfigWindow diff --git a/src/main/nativeWindowChrome.ts b/src/main/nativeWindowChrome.ts new file mode 100644 index 0000000..c499a8f --- /dev/null +++ b/src/main/nativeWindowChrome.ts @@ -0,0 +1,37 @@ +import { createRequire } from 'node:module' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import type { VisualizerDSP } from '../renderer/audio/native/visualizer-dsp' +import type { NativeCaptureAPI } from '../types/nativeCapture' +import type { NativeWindowChromeAPI } from '../types/nativeWindowChrome' + +type NativeAddonModule = VisualizerDSP & NativeCaptureAPI & { + windowChrome?: NativeWindowChromeAPI +} + +const require = createRequire(import.meta.url) +const currentDir = dirname(fileURLToPath(import.meta.url)) + +export function loadNativeWindowChromeApi(): NativeWindowChromeAPI | null { + if (process.platform !== 'win32') { + return null + } + + const candidatePaths = [ + join(currentDir, '../../native/build/Release/visualizer_dsp.node'), + process.resourcesPath ? join(process.resourcesPath, 'native/visualizer_dsp.node') : null, + ].filter((value): value is string => value !== null) + + for (const modulePath of candidatePaths) { + try { + const nativeAddon = require(modulePath) as NativeAddonModule + if (nativeAddon.windowChrome) { + return nativeAddon.windowChrome + } + } catch { + // Try the next candidate path. + } + } + + return null +} diff --git a/src/types/nativeWindowChrome.ts b/src/types/nativeWindowChrome.ts new file mode 100644 index 0000000..e98c2d3 --- /dev/null +++ b/src/types/nativeWindowChrome.ts @@ -0,0 +1,7 @@ +export interface NativeWindowChromeAPI { + // Strips the visible native frame/shadow from a frameless WS_THICKFRAME window + // while preserving native snapping and edge resize. Returns false if the handle + // was invalid or the subclass could not be installed. Windows-only; a no-op + // elsewhere. + applyFlatFrame: (nativeWindowHandle: Buffer) => boolean +}