Match window border color to app theme rather than OS theme (#958)

* MacOS: match window chrome to app light/dark mode when app setting is different from OS

* match window border to app theme on Windows
This commit is contained in:
Drew Weymouth
2026-06-24 13:46:09 -07:00
committed by GitHub
parent 2774970ca8
commit 67623366f5
9 changed files with 139 additions and 0 deletions
+21
View File
@@ -10,6 +10,7 @@ import (
"log"
"os"
"path/filepath"
"runtime"
"time"
fynetooltip "github.com/dweymouth/fyne-tooltip"
@@ -26,6 +27,7 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/driver"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
@@ -655,3 +657,22 @@ func (c *Controller) GetSongRadioTracks(sourceTrack *mediaprovider.Track) ([]*me
tracks = append(tracks, filteredTracks...)
return tracks, nil
}
func SetWindowThemeMode(win fyne.Window, mode myTheme.AppearanceMode) {
arg := 0
switch fyne.CurrentApp().Settings().Theme().(*myTheme.MyTheme).AppearanceMode() {
case myTheme.AppearanceDark:
arg = 1
case myTheme.AppearanceLight:
arg = 2
}
win.(driver.NativeWindow).RunNative(func(ctx any) {
switch runtime.GOOS {
case "darwin":
// ensure dark mode setting is applied to window controls in title bar on Mac
setWindowDarkTheme(ctx.(driver.MacWindowContext).NSWindow, arg)
case "windows":
setWindowDarkTheme(ctx.(driver.WindowsWindowContext).HWND, arg)
}
})
}
+2
View File
@@ -9,6 +9,7 @@ import (
"github.com/dweymouth/supersonic/backend/player"
"github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/ui/shortcuts"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
"github.com/dweymouth/supersonic/ui/visualizations"
)
@@ -68,6 +69,7 @@ func (c *Controller) ShowPeakMeter() {
c.peakMeter.Refresh()
}
c.peakMeterWin.Show()
SetWindowThemeMode(c.peakMeterWin, fyne.CurrentApp().Settings().Theme().(*myTheme.MyTheme).AppearanceMode())
}
func (c *Controller) stopVisualizationAnim() {
+20
View File
@@ -0,0 +1,20 @@
//go:build darwin
#import <AppKit/AppKit.h>
void setWindowDarkMode(void* windowPtr, int mode) {
if (windowPtr == NULL) return;
NSWindow* window = (__bridge NSWindow*)windowPtr;
if (mode == 1) {
// Forces the window chrome and borders into Dark Mode
window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark];
} else if (mode == 2) {
// Forces the window chrome and borders into Light Mode
window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameVibrantLight];
} else {
// Reverts the window back to standard/system default behavior
window.appearance = nil; // Fallback to inherited system style
}
}
+16
View File
@@ -0,0 +1,16 @@
//go:build darwin
package controller
/*
void setWindowDarkMode(void* windowPtr, int useDarkMode);
*/
import "C"
import "unsafe"
// setWindowDarkTheme takes an unsafe.Pointer referencing the macOS NSWindow
// and explicitly applies or clears the dark mode appearance.
// Mode arg: 0 = auto, 1 = dark, 2 = light
func setWindowDarkTheme(nsWindowPtr uintptr, mode int) {
C.setWindowDarkMode(unsafe.Pointer(nsWindowPtr), C.int(mode))
}
+5
View File
@@ -0,0 +1,5 @@
//go:build !darwin && !windows
package controller
func setWindowDarkTheme(ptr uintptr, mode int) {}
+57
View File
@@ -0,0 +1,57 @@
//go:build windows
package controller
import (
"runtime"
"syscall"
"unsafe"
"fyne.io/fyne/v2"
"golang.org/x/sys/windows/registry"
)
var (
dwm = syscall.NewLazyDLL("dwmapi.dll")
setAtt = dwm.NewProc("DwmSetWindowAttribute")
)
func setWindowDarkTheme(hwnd uintptr, mode int) {
if runtime.GOOS != "windows" {
return
}
arg := 0
switch mode {
case 0: /*auto*/
if isDark() {
arg = 1
}
case 1: /*dark*/
arg = 1
}
// copied from Fyne internals
ret, _, err := setAtt.Call(uintptr(unsafe.Pointer(hwnd)), // window handle
20, // DWMWA_USE_IMMERSIVE_DARK_MODE
uintptr(unsafe.Pointer(&arg)), // on or off
4) // sizeof(bool for windows))
if ret != 0 && ret != 0x80070057 { // err is always non-nil, we check return value (except erroneous code)
fyne.LogError("Failed to set dark mode", err)
}
}
// copied from Fyne internals
func isDark() bool {
k, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE)
if err != nil { // older version of Windows will not have this key
return false
}
defer k.Close()
useLight, _, err := k.GetIntegerValue("AppsUseLightTheme")
if err != nil { // older version of Windows will not have this value
return false
}
return useLight == 0
}
+3
View File
@@ -557,6 +557,9 @@ func (m *MainWindow) addShortcuts() {
func (m *MainWindow) showSettingsDialog() {
m.Controller.ShowSettingsDialog(func() {
fyne.CurrentApp().Settings().SetTheme(m.theme)
for _, w := range fyne.CurrentApp().Driver().AllWindows() {
controller.SetWindowThemeMode(w, m.theme.AppearanceMode())
}
}, m.theme.ListThemeFiles())
}
+10
View File
@@ -115,6 +115,16 @@ func (m *MyTheme) ReloadThemeFile() {
m.loadedThemeFile = nil
}
func (m *MyTheme) AppearanceMode() AppearanceMode {
v := DefaultAppearance // default if config has invalid or missing setting
if slices.Contains(
[]string{string(AppearanceLight), string(AppearanceDark), string(AppearanceAuto)},
m.config.Appearance) {
v = AppearanceMode(m.config.Appearance)
}
return v
}
func (m *MyTheme) Color(name fyne.ThemeColorName, defVariant fyne.ThemeVariant) color.Color {
// load theme file if necessary
if m.loadedThemeFile == nil || m.config.ThemeFile != m.loadedThemeFilename {