[client] Add light mode with system, light, and dark theme options (#7344)

* desktop UI light mode

* Theme review fixes plus macOS window outline fix

* Windows runtime chrome re-theming plus apply serialization

* Windows chrome threading and theme event ordering fixes

* Darken toggle and setting sidebar text

* resolve theme appearance, apply on UI thread

* read theme once per window

* Re-assert Windows dark opt-in after SetTheme

* split app-wide GTK theming from per-window chrome

* Update Wails dependency and checksums

* KDE tray icon panel fix

* Five review fixes: theme ordering, cgo dedup, KDE panel resolution

* Path guard hardening, toggle contrast, windows comment

* non-vacuous escape tests

* Default view edits

* Polish settings nav, controls, borders, and disc

* Profiles settings boarder, modals, and buttons

* Additional edits based on feedback

* Switch colors away from slight blue hue

* Update missing lang

* Fix vertical tab active view
This commit is contained in:
Brandon Hopkins
2026-09-11 08:25:10 -07:00
committed by GitHub
parent f422c41654
commit ec0c36b0e7
68 changed files with 1853 additions and 259 deletions
+99
View File
@@ -0,0 +1,99 @@
//go:build linux && cgo && !android && !ios
package services
import (
"os"
"path/filepath"
"strings"
)
// setAppAppearance points GTK at the light or dark variant of the current theme
// so the decorations match the webview. Without it a forced Light theme keeps
// dark decorations on a dark desktop, and the reverse.
//
// The theme name is switched, not just gtk-application-prefer-dark-theme:
// desktops such as Ubuntu implement dark mode as a separate theme (Yaru-dark),
// which that flag cannot lighten. The flag is still set for themes that do
// carry both variants under one name. Both are per-process settings, so this
// changes only our own decorations; GTK re-reads the desktop value on a change,
// which is why Theme.apply re-asserts. Must run on the main thread.
//
// GTK styling is app-wide, which is why this is separate from
// setWindowAppearance: it must be applied even when no window exists yet, since
// windows created later inherit it rather than carrying it in their options.
func setAppAppearance(dark bool) {
target := baseGtkTheme(gtkThemeName())
if dark {
if variant, ok := darkGtkVariant(target); ok {
target = variant
}
}
// An unknown name would leave GTK with no theme at all, so fall back to
// changing nothing and let the prefer-dark flag do what it can.
if target != "" && !gtkThemeExists(target) {
target = ""
}
applyGtkTheme(target, dark)
}
// baseGtkTheme strips a dark-variant suffix, so "Yaru-dark" becomes "Yaru".
func baseGtkTheme(name string) string {
for _, suffix := range []string{"-dark", "-Dark"} {
if len(name) > len(suffix) && strings.EqualFold(name[len(name)-len(suffix):], suffix) {
return name[:len(name)-len(suffix)]
}
}
return name
}
// darkGtkVariant reports the installed dark counterpart of a base theme name.
// Themes that carry both variants under one name have none, and rely on
// gtk-application-prefer-dark-theme instead.
func darkGtkVariant(base string) (string, bool) {
if base == "" {
return "", false
}
for _, suffix := range []string{"-dark", "-Dark"} {
if candidate := base + suffix; gtkThemeExists(candidate) {
return candidate, true
}
}
return "", false
}
// gtkThemeExists reports whether a theme of that name is installed, searching
// the same locations GTK does.
func gtkThemeExists(name string) bool {
if name == "" {
return false
}
for _, dir := range gtkThemeDirs() {
if info, err := os.Stat(filepath.Join(dir, name)); err == nil && info.IsDir() {
return true
}
}
return false
}
func gtkThemeDirs() []string {
var dirs []string
if home, err := os.UserHomeDir(); err == nil {
dirs = append(dirs, filepath.Join(home, ".themes"))
}
if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome != "" {
dirs = append(dirs, filepath.Join(dataHome, "themes"))
} else if home, err := os.UserHomeDir(); err == nil {
dirs = append(dirs, filepath.Join(home, ".local", "share", "themes"))
}
dataDirs := os.Getenv("XDG_DATA_DIRS")
if dataDirs == "" {
dataDirs = "/usr/local/share:/usr/share"
}
for _, dir := range strings.Split(dataDirs, ":") {
if dir != "" {
dirs = append(dirs, filepath.Join(dir, "themes"))
}
}
return dirs
}
@@ -0,0 +1,63 @@
//go:build linux && cgo && !android && !ios
package services
/*
// The GTK major version is the only difference between the two Linux builds, so
// it is selected by these two directives rather than by keeping a second copy of
// this file per version: the C below and the Go wrappers under it are identical
// for GTK3 and GTK4, and both resolve <gtk/gtk.h> through pkg-config.
#cgo gtk3 pkg-config: gtk+-3.0
#cgo !gtk3 pkg-config: gtk4
#include <stdlib.h>
#include <gtk/gtk.h>
static char *nbGetGtkThemeName(void) {
GtkSettings *settings = gtk_settings_get_default();
if (settings == NULL) {
return NULL;
}
char *name = NULL;
g_object_get(settings, "gtk-theme-name", &name, NULL);
return name;
}
// name may be NULL to leave the theme name untouched.
static void nbSetGtkTheme(const char *name, int dark) {
GtkSettings *settings = gtk_settings_get_default();
if (settings == NULL) {
return;
}
if (name != NULL && name[0] != '\0') {
g_object_set(settings, "gtk-theme-name", name, NULL);
}
g_object_set(settings, "gtk-application-prefer-dark-theme", dark ? TRUE : FALSE, NULL);
}
static void nbFreeGtkString(char *s) { g_free(s); }
*/
import "C"
import "unsafe"
func gtkThemeName() string {
c := C.nbGetGtkThemeName()
if c == nil {
return ""
}
defer C.nbFreeGtkString(c)
return C.GoString(c)
}
func applyGtkTheme(name string, dark bool) {
var cName *C.char
if name != "" {
cName = C.CString(name)
defer C.free(unsafe.Pointer(cName))
}
var forced C.int
if dark {
forced = 1
}
C.nbSetGtkTheme(cName, forced)
}
@@ -0,0 +1,7 @@
//go:build !(linux && cgo)
package services
// setAppAppearance is a no-op where the platform has no app-wide appearance to
// set; macOS and Windows theme each window instead, via setWindowAppearance.
func setAppAppearance(bool) {}
+4
View File
@@ -31,6 +31,10 @@ func (s *Preferences) SetViewMode(_ context.Context, mode preferences.ViewMode)
return s.store.SetViewMode(mode)
}
func (s *Preferences) SetTheme(_ context.Context, theme preferences.Theme) error {
return s.store.SetTheme(theme)
}
func (s *Preferences) SetOnboardingCompleted(_ context.Context, done bool) error {
return s.store.SetOnboardingCompleted(done)
}
+194
View File
@@ -0,0 +1,194 @@
//go:build !android && !ios && !freebsd && !js
package services
import (
"context"
"sync"
"sync/atomic"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/netbirdio/netbird/client/ui/preferences"
)
// EventSystemThemeChanged fires when the OS appearance flips, payload SystemTheme.
// The frontend resolves the "system" preference against it.
const EventSystemThemeChanged = "netbird:system-theme:changed"
// SystemTheme is the EventSystemThemeChanged payload.
type SystemTheme struct {
Dark bool `json:"dark"`
}
// Theme keeps native window background colours in step with the persisted
// theme preference so no window flashes the wrong surface before the webview
// paints. The frontend applies the matching .dark class via ThemeContext.
type Theme struct {
app *application.App
store *preferences.Store
// mu serializes apply: concurrent callers could otherwise enqueue a stale
// pref's native updates after a newer one's.
mu sync.Mutex
// started gates the main-thread dispatch in apply: Run installs the platform
// layer InvokeAsync needs, and the store subscription can fire before that.
started atomic.Bool
}
// NewTheme wires the store subscription and OS theme-change listener. Call
// before any window is created so creation-time colours are already themed.
func NewTheme(app *application.App, store *preferences.Store) *Theme {
t := &Theme{app: app, store: store}
pref := store.Get().Theme
setAppearance(pref, resolveDark(pref, app.Env.IsDarkMode()))
// Window creation resolves through this rather than the seed above, which
// is wrong until Run installs the platform layer: Env.IsDarkMode reports
// light before that, so a "system" launch on a dark OS would build the
// first window light. The ApplicationStarted apply below cannot be relied
// on to land first because Wails runs each listener in its own goroutine.
// One store read backs both fields, so the snapshot is always self-consistent.
setAppearanceResolver(func() Appearance {
p := t.store.Get().Theme
return Appearance{Pref: p, Dark: resolveDark(p, t.app.Env.IsDarkMode())}
})
ch, _ := store.Subscribe()
go func() {
var last preferences.Theme
for p := range ch {
if p.Theme == last {
continue
}
last = p.Theme
t.apply()
}
}()
// Re-apply on every OS flip, not just for ThemeSystem: Windows re-evaluates
// process-level theme state on WM_SETTINGCHANGE, so a forced theme has to be
// re-asserted or the native chrome drifts to the OS appearance. The event's
// own IsDarkMode is deliberately unused: Wails runs each application event
// handler in its own goroutine, so two rapid flips race, and apply re-reads
// the appearance under mu instead.
app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(*application.ApplicationEvent) {
t.apply()
})
// Startup is split in two because Wails runs every application-event
// listener in its own goroutine, so a listener cannot be ordered against the
// one that opens the first-launch window. Hooks can: they run sequentially,
// in registration order, and all of them before any listener is spawned.
//
// The app-wide GTK theme goes in the hook because it is the part a window
// must not be created without. On Linux it draws the decorations and
// application.LinuxWindow carries no theme of its own, so a window built
// before it lands shows OS-coloured decorations until it does. It is applied
// synchronously for the same reason -- returning from the hook has to mean
// the theme is live. This relies on the listener below existing: Wails skips
// an event's hooks entirely when it has no listeners.
app.Event.RegisterApplicationEventHook(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
t.started.Store(true)
t.syncAppAppearance()
})
// The rest of the startup apply. Env.IsDarkMode is a stub until the platform
// layer is up, so re-resolve once the app has started or a "system" launch on
// a light OS stays seeded dark.
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
t.apply()
})
return t
}
// syncAppAppearance applies the app-wide appearance and waits for the UI thread
// to have done it. Use it where a window is about to be created and must not be
// built against the OS appearance: apply dispatches its own native work
// asynchronously, so on Linux the GTK theme behind the decorations can otherwise
// land after the window exists.
//
// No-op before the app has started, where InvokeSync has no platform layer to
// dispatch to. Reads the appearance under mu like apply, so the two cannot
// interleave into a torn update.
func (t *Theme) syncAppAppearance() {
if !t.started.Load() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
pref := t.store.Get().Theme
dark := resolveDark(pref, t.app.Env.IsDarkMode())
setAppearance(pref, dark)
application.InvokeSync(func() { setAppAppearance(dark) })
}
// SystemDarkMode reports the OS appearance; bound so the frontend can resolve
// the "system" preference from the same source as the native layer.
func (t *Theme) SystemDarkMode(_ context.Context) (bool, error) {
return t.app.Env.IsDarkMode(), nil
}
// resolveDark maps a preference to an effective appearance against a system
// reading the caller already took.
func resolveDark(pref preferences.Theme, systemDark bool) bool {
switch pref {
case preferences.ThemeDark:
return true
case preferences.ThemeLight:
return false
default:
return systemDark
}
}
// apply recomputes the effective appearance, re-tints every live window
// (including the macOS NSWindow appearance so the frame matches the webview)
// and publishes the system appearance the frontend resolves "system" against.
//
// Everything runs under mu and reads the appearance here rather than taking it
// from a caller, so a later apply always carries the fresher state and the
// frontend event is ordered by the same lock as the native assignments. Emit
// only appends to a FIFO mailbox, so holding mu across it cannot block.
//
// The OS is read exactly once per update and the resolved value is passed on to
// the background and the native chrome, so those cannot land on either side of
// an OS flip that happens mid-apply. The event carries the raw system reading,
// not the resolved one, because the frontend resolves "system" itself.
func (t *Theme) apply() {
t.mu.Lock()
defer t.mu.Unlock()
pref := t.store.Get().Theme
systemDark := t.app.Env.IsDarkMode()
dark := resolveDark(pref, systemDark)
setAppearance(pref, dark)
t.app.Event.Emit(EventSystemThemeChanged, SystemTheme{Dark: systemDark})
// Before Run there is no platform layer for InvokeAsync to dispatch to.
// Windows created later read the globals set above.
if !t.started.Load() {
return
}
colour := windowBackgroundColour(dark)
// Re-tint on the UI thread and resolve each native handle there. Window
// teardown (markAsDestroyed then impl.close) runs as UI-thread work too, so
// a window closed meanwhile is either gone from GetAll or yields a nil
// handle -- never a freed handle the OS may already have reused.
application.InvokeAsync(func() {
// App-wide first, and unconditionally: on Linux this is the GTK theme
// that draws the decorations, and it must be set even with no window
// open because later windows inherit it instead of carrying it.
setAppAppearance(dark)
for _, w := range t.app.Window.GetAll() {
if w == nil {
continue
}
w.SetBackgroundColour(colour)
setWindowAppearance(w.NativeWindow(), pref, dark)
}
})
}
@@ -0,0 +1,53 @@
package services
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework AppKit
#import <AppKit/AppKit.h>
// forced < 0: follow the OS (appearance nil); 0: light; 1: dark.
//
// Assigns directly rather than dispatching: Theme.apply already runs this on
// the main thread. Deferring would outlive the caller's check that the window
// is alive, and the __bridge cast does not retain it, so the block could touch
// a freed NSWindow.
static void nbSetWindowAppearance(void *nsWindow, int forced) {
NSWindow *window = (__bridge NSWindow *)nsWindow;
if (forced < 0) {
window.appearance = nil;
} else {
NSAppearanceName name = forced == 1 ? NSAppearanceNameDarkAqua : NSAppearanceNameAqua;
window.appearance = [NSAppearance appearanceNamed:name];
}
}
*/
import "C"
import (
"unsafe"
"github.com/netbirdio/netbird/client/ui/preferences"
)
// setWindowAppearance pins the NSWindow appearance to the forced theme, or
// hands it back to the OS for ThemeSystem. Without this, a window created
// under one OS appearance keeps its dark/light frame after a manual theme
// flip, leaving a mismatched border around the webview. Must run on the main
// thread, which Theme.apply guarantees.
//
// The resolved appearance is unused: for ThemeSystem a nil NSAppearance lets
// AppKit track the OS itself, which cannot drift from a snapshot we took.
func setWindowAppearance(nsWindow unsafe.Pointer, pref preferences.Theme, _ bool) {
if nsWindow == nil {
return
}
forced := C.int(-1)
switch pref {
case preferences.ThemeLight:
forced = 0
case preferences.ThemeDark:
forced = 1
}
C.nbSetWindowAppearance(nsWindow, forced)
}
@@ -0,0 +1,16 @@
//go:build !darwin && !windows && !android && !ios && !freebsd && !js
package services
import (
"unsafe"
"github.com/netbirdio/netbird/client/ui/preferences"
)
// setWindowAppearance is a no-op wherever there is no per-window appearance to
// set, which is every target this file covers. On Linux the appearance is real
// but app-wide, so setAppAppearance owns it instead; on the remaining Unix
// targets there is no native theming to apply at all and setAppAppearance is
// itself a stub (appappearance_other.go).
func setWindowAppearance(unsafe.Pointer, preferences.Theme, bool) {}
@@ -0,0 +1,54 @@
package services
import (
"unsafe"
"github.com/wailsapp/wails/v3/pkg/w32"
"github.com/netbirdio/netbird/client/ui/preferences"
)
// setWindowAppearance re-themes a live window's chrome; Wails only does this
// itself on OS flips for SystemDefault windows.
//
// Must run on the UI thread, which Theme.apply guarantees: the uxtheme and
// repaint calls behind w32.SetTheme belong to the window's thread, and hwnd is
// only known live while we hold that thread. Re-dispatching here would let the
// window be destroyed first and hand these writes a reused handle.
//
// dark is the appearance Theme.apply already resolved. Re-reading the OS here
// would let the chrome land on the other side of an OS flip from the window
// background and the webview.
func setWindowAppearance(hwnd unsafe.Pointer, _ preferences.Theme, dark bool) {
if hwnd == nil || !w32.SupportsThemes() || w32.IsCurrentlyHighContrastMode() {
return
}
h := uintptr(hwnd)
w32.SetTheme(h, dark)
// After SetTheme, not before: its menu helper regates dark on the
// process-level ShouldAppsUseDarkMode and rewrites the per-window opt-in
// with that gated value, so forcing Dark on a light OS would lose it --
// and builds below 18985 need the opt-in for the pre-20H1 dark frame. The
// gated menu theme name is left alone on purpose: these windows carry no
// native menu, and popup-menu text follows the process policy, so forcing
// it dark gives dark text on dark.
if w32.AllowDarkModeForWindow != nil {
w32.AllowDarkModeForWindow(h, dark)
}
chrome := microsoftWindowsLightTheme
if dark {
chrome = microsoftWindowsDarkTheme
}
if chrome.TitleBarColour != nil {
w32.SetTitleBarColour(h, *chrome.TitleBarColour)
}
if chrome.TitleTextColour != nil {
w32.SetTitleTextColour(h, *chrome.TitleTextColour)
}
if chrome.BorderColour != nil {
w32.SetBorderColour(h, *chrome.BorderColour)
}
}
+105 -18
View File
@@ -6,6 +6,7 @@ import (
"net/url"
"strconv"
"sync"
"sync/atomic"
"time"
log "github.com/sirupsen/logrus"
@@ -36,39 +37,123 @@ const paintedFallback = 2 * time.Second
const headlessTeardownDelay = 2 * time.Second
var WindowBackgroundColour = application.NewRGB(24, 26, 29) // bg-nb-gray-950
// Window background per effective appearance. Both match the body background
// (bg-nb-gray DEFAULT) in globals.css so opaque native pixels and the webview
// paint the same surface; keep the three in sync.
var (
windowBackgroundDark = application.NewRGB(24, 26, 29) // dark nb-gray DEFAULT
windowBackgroundLight = application.NewRGB(243, 243, 243) // light nb-gray DEFAULT
)
// Appearance is one view of the theme state: the preference and the appearance
// it resolves to. Take it once per window with CurrentAppearance and pass the
// same value to every option builder -- Pref drives the macOS frame while Dark
// drives the background and the Windows chrome, so reading them separately can
// build a window with a new background behind the previous native frame.
type Appearance struct {
Pref preferences.Theme
Dark bool
}
// storedAppearance is the snapshot maintained by services.Theme, published as
// one value so the pair can never tear. It is the fallback for window creation
// until resolveAppearance is installed.
var storedAppearance atomic.Value // Appearance
// resolveAppearance re-resolves against the live OS state. Theme installs it so
// window creation never reads a stale seed: app.Env.IsDarkMode reports light
// until Run installs the platform layer, and Wails runs every
// ApplicationStarted listener in its own goroutine, so a startup window can be
// created before Theme's listener has corrected the seed.
var resolveAppearance atomic.Value // func() Appearance
func init() {
storedAppearance.Store(Appearance{Pref: preferences.DefaultTheme, Dark: true})
}
func setAppearance(pref preferences.Theme, dark bool) {
storedAppearance.Store(Appearance{Pref: pref, Dark: dark})
}
func setAppearanceResolver(f func() Appearance) { resolveAppearance.Store(f) }
// CurrentAppearance returns the snapshot every window creation must build from.
func CurrentAppearance() Appearance {
if f, _ := resolveAppearance.Load().(func() Appearance); f != nil {
return f()
}
a, _ := storedAppearance.Load().(Appearance)
return a
}
// WindowBackgroundColour returns the background for a snapshot; use it for
// every WebviewWindowOptions.BackgroundColour.
func WindowBackgroundColour(a Appearance) application.RGBA {
return windowBackgroundColour(a.Dark)
}
// windowBackgroundColour maps a resolved appearance to its window background.
func windowBackgroundColour(dark bool) application.RGBA {
if dark {
return windowBackgroundDark
}
return windowBackgroundLight
}
// WindowHeight is shared by the main and Settings windows.
const WindowHeight = 660
// Wails reads CustomTheme colours as 0x00BBGGRR (RGB byte order reversed).
var microsoftWindowsTheme = &application.WindowTheme{
BorderColour: u32ptr(0x00211E1C),
var microsoftWindowsDarkTheme = &application.WindowTheme{
BorderColour: u32ptr(0x00211E1C), // #1C1E21 nb-gray-940
TitleBarColour: u32ptr(0x00211E1C),
TitleTextColour: u32ptr(0x00E9E7E4),
TitleTextColour: u32ptr(0x00E9E7E4), // #E4E7E9 nb-gray-100
}
// MicrosoftWindowsAppearanceOptions is the shared Windows chrome (Mica + dark + custom title bar).
func MicrosoftWindowsAppearanceOptions() application.WindowsWindow {
var microsoftWindowsLightTheme = &application.WindowTheme{
BorderColour: u32ptr(0x00F3F3F3), // #F3F3F3 light nb-gray DEFAULT
TitleBarColour: u32ptr(0x00F3F3F3),
TitleTextColour: u32ptr(0x00212121), // #212121 light nb-gray-100
}
// MicrosoftWindowsAppearanceOptions is the shared Windows chrome (Mica +
// custom title bar), resolved at creation; setWindowAppearance re-themes live
// windows on later changes. Never SystemDefault: Wails gives those windows a
// SystemThemeChanged handler that re-themes chrome from the OS appearance,
// which outlives a switch to a forced theme and fights it on the next OS flip.
// Both CustomTheme slots hold one colour set for the same reason.
func MicrosoftWindowsAppearanceOptions(a Appearance) application.WindowsWindow {
theme, chrome := application.Light, microsoftWindowsLightTheme
if a.Dark {
theme, chrome = application.Dark, microsoftWindowsDarkTheme
}
return application.WindowsWindow{
BackdropType: application.Mica,
Theme: application.Dark,
Theme: theme,
CustomTheme: application.ThemeSettings{
DarkModeActive: microsoftWindowsTheme,
DarkModeInactive: microsoftWindowsTheme,
LightModeActive: microsoftWindowsTheme,
LightModeInactive: microsoftWindowsTheme,
DarkModeActive: chrome,
DarkModeInactive: chrome,
LightModeActive: chrome,
LightModeInactive: chrome,
},
}
}
// AppleMacOSAppearanceOptions is the shared macOS chrome; FullScreenNone keeps the fixed-size layout.
func AppleMacOSAppearanceOptions() application.MacWindow {
func AppleMacOSAppearanceOptions(a Appearance) application.MacWindow {
appearance := application.DefaultAppearance
switch a.Pref {
case preferences.ThemeLight:
appearance = application.NSAppearanceNameAqua
case preferences.ThemeDark:
appearance = application.NSAppearanceNameDarkAqua
}
return application.MacWindow{
InvisibleTitleBarHeight: 38,
Backdrop: application.MacBackdropNormal,
TitleBar: application.MacTitleBarHiddenInset,
CollectionBehavior: application.MacWindowCollectionBehaviorFullScreenNone,
Appearance: appearance,
}
}
@@ -82,6 +167,7 @@ func LinuxAppearanceOptions(icon []byte) application.LinuxWindow {
// DialogWindowOptions is the baseline for every auxiliary dialog window; callers override per-dialog.
func DialogWindowOptions(name, title, url string, linuxIcon []byte) application.WebviewWindowOptions {
a := CurrentAppearance()
return application.WebviewWindowOptions{
Name: name,
Title: title,
@@ -93,10 +179,10 @@ func DialogWindowOptions(name, title, url string, linuxIcon []byte) application.
MinimiseButtonState: application.ButtonHidden,
MaximiseButtonState: application.ButtonHidden,
CloseButtonState: application.ButtonEnabled,
BackgroundColour: WindowBackgroundColour,
BackgroundColour: WindowBackgroundColour(a),
URL: url,
Mac: AppleMacOSAppearanceOptions(),
Windows: MicrosoftWindowsAppearanceOptions(),
Mac: AppleMacOSAppearanceOptions(a),
Windows: MicrosoftWindowsAppearanceOptions(a),
Linux: LinuxAppearanceOptions(linuxIcon),
}
}
@@ -164,6 +250,7 @@ func NewWindowManager(app *application.App, mainWindow *application.WebviewWindo
}
func (s *WindowManager) newSettingsWindow() *application.WebviewWindow {
a := CurrentAppearance()
w := s.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: s.title("window.title.settings"),
@@ -174,10 +261,10 @@ func (s *WindowManager) newSettingsWindow() *application.WebviewWindow {
MinimiseButtonState: application.ButtonHidden,
MaximiseButtonState: application.ButtonHidden,
CloseButtonState: application.ButtonEnabled,
BackgroundColour: WindowBackgroundColour,
BackgroundColour: WindowBackgroundColour(a),
URL: "/#/settings",
Mac: AppleMacOSAppearanceOptions(),
Windows: MicrosoftWindowsAppearanceOptions(),
Mac: AppleMacOSAppearanceOptions(a),
Windows: MicrosoftWindowsAppearanceOptions(a),
Linux: LinuxAppearanceOptions(s.linuxIcon),
})
w.RegisterHook(events.Common.WindowClosing, func(_ *application.WindowEvent) {