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:
@@ -16,6 +16,8 @@ import (
|
|||||||
"github.com/dweymouth/supersonic/res"
|
"github.com/dweymouth/supersonic/res"
|
||||||
"github.com/dweymouth/supersonic/res/wintaskbarthumbs"
|
"github.com/dweymouth/supersonic/res/wintaskbarthumbs"
|
||||||
"github.com/dweymouth/supersonic/ui"
|
"github.com/dweymouth/supersonic/ui"
|
||||||
|
"github.com/dweymouth/supersonic/ui/controller"
|
||||||
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
|
||||||
@@ -133,6 +135,9 @@ func main() {
|
|||||||
fyneApp.Run()
|
fyneApp.Run()
|
||||||
} else {
|
} else {
|
||||||
fyneApp.Lifecycle().SetOnStarted(func() {
|
fyneApp.Lifecycle().SetOnStarted(func() {
|
||||||
|
if mode := fyne.CurrentApp().Settings().Theme().(*myTheme.MyTheme).AppearanceMode(); mode != myTheme.AppearanceAuto {
|
||||||
|
controller.SetWindowThemeMode(mainWindow.Window, mode)
|
||||||
|
}
|
||||||
defaultServer := myApp.ServerManager.GetDefaultServer()
|
defaultServer := myApp.ServerManager.GetDefaultServer()
|
||||||
if defaultServer == nil {
|
if defaultServer == nil {
|
||||||
mainWindow.Controller.PromptForFirstServer()
|
mainWindow.Controller.PromptForFirstServer()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
fynetooltip "github.com/dweymouth/fyne-tooltip"
|
fynetooltip "github.com/dweymouth/fyne-tooltip"
|
||||||
@@ -26,6 +27,7 @@ import (
|
|||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/dialog"
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/driver"
|
||||||
"fyne.io/fyne/v2/lang"
|
"fyne.io/fyne/v2/lang"
|
||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
@@ -655,3 +657,22 @@ func (c *Controller) GetSongRadioTracks(sourceTrack *mediaprovider.Track) ([]*me
|
|||||||
tracks = append(tracks, filteredTracks...)
|
tracks = append(tracks, filteredTracks...)
|
||||||
return tracks, nil
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/dweymouth/supersonic/backend/player"
|
"github.com/dweymouth/supersonic/backend/player"
|
||||||
"github.com/dweymouth/supersonic/backend/player/mpv"
|
"github.com/dweymouth/supersonic/backend/player/mpv"
|
||||||
"github.com/dweymouth/supersonic/ui/shortcuts"
|
"github.com/dweymouth/supersonic/ui/shortcuts"
|
||||||
|
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||||
"github.com/dweymouth/supersonic/ui/util"
|
"github.com/dweymouth/supersonic/ui/util"
|
||||||
"github.com/dweymouth/supersonic/ui/visualizations"
|
"github.com/dweymouth/supersonic/ui/visualizations"
|
||||||
)
|
)
|
||||||
@@ -68,6 +69,7 @@ func (c *Controller) ShowPeakMeter() {
|
|||||||
c.peakMeter.Refresh()
|
c.peakMeter.Refresh()
|
||||||
}
|
}
|
||||||
c.peakMeterWin.Show()
|
c.peakMeterWin.Show()
|
||||||
|
SetWindowThemeMode(c.peakMeterWin, fyne.CurrentApp().Settings().Theme().(*myTheme.MyTheme).AppearanceMode())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) stopVisualizationAnim() {
|
func (c *Controller) stopVisualizationAnim() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build !darwin && !windows
|
||||||
|
|
||||||
|
package controller
|
||||||
|
|
||||||
|
func setWindowDarkTheme(ptr uintptr, mode int) {}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -557,6 +557,9 @@ func (m *MainWindow) addShortcuts() {
|
|||||||
func (m *MainWindow) showSettingsDialog() {
|
func (m *MainWindow) showSettingsDialog() {
|
||||||
m.Controller.ShowSettingsDialog(func() {
|
m.Controller.ShowSettingsDialog(func() {
|
||||||
fyne.CurrentApp().Settings().SetTheme(m.theme)
|
fyne.CurrentApp().Settings().SetTheme(m.theme)
|
||||||
|
for _, w := range fyne.CurrentApp().Driver().AllWindows() {
|
||||||
|
controller.SetWindowThemeMode(w, m.theme.AppearanceMode())
|
||||||
|
}
|
||||||
}, m.theme.ListThemeFiles())
|
}, m.theme.ListThemeFiles())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,16 @@ func (m *MyTheme) ReloadThemeFile() {
|
|||||||
m.loadedThemeFile = nil
|
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 {
|
func (m *MyTheme) Color(name fyne.ThemeColorName, defVariant fyne.ThemeVariant) color.Color {
|
||||||
// load theme file if necessary
|
// load theme file if necessary
|
||||||
if m.loadedThemeFile == nil || m.config.ThemeFile != m.loadedThemeFilename {
|
if m.loadedThemeFile == nil || m.config.ThemeFile != m.loadedThemeFilename {
|
||||||
|
|||||||
Reference in New Issue
Block a user