[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
+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) {