From 48254b73e5e843abe11a931eeb3d1cf2d1214c07 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sun, 27 Mar 2022 22:57:45 +1100 Subject: [PATCH] [Feature/1149] Dark mode (#1281) * Add Windows version helper * Initial theme support * Support custom themes * Update docs * Honour HighContrast theme. Remove import "C". Refactor * Small refactor * Support inactive theme * Update Docs --- v2/go.mod | 2 +- v2/go.sum | 2 + .../frontend/desktop/windows/frontend.go | 10 +- v2/internal/frontend/desktop/windows/menu.go | 6 +- v2/internal/frontend/desktop/windows/theme.go | 62 +++++++++++++ .../frontend/desktop/windows/win32/consts.go | 27 ++++++ .../frontend/desktop/windows/win32/theme.go | 92 +++++++++++++++++++ .../frontend/desktop/windows/win32/window.go | 56 +++++++++++ .../frontend/desktop/windows/window.go | 64 ++++++------- .../system/operatingsystem/version_windows.go | 59 ++++++++++++ v2/pkg/options/windows/windows.go | 41 +++++++++ website/docs/reference/options.mdx | 81 ++++++++++++++++ 12 files changed, 463 insertions(+), 39 deletions(-) create mode 100644 v2/internal/frontend/desktop/windows/theme.go create mode 100644 v2/internal/frontend/desktop/windows/win32/consts.go create mode 100644 v2/internal/frontend/desktop/windows/win32/theme.go create mode 100644 v2/internal/frontend/desktop/windows/win32/window.go create mode 100644 v2/internal/system/operatingsystem/version_windows.go diff --git a/v2/go.mod b/v2/go.mod index 3d6d4ae8c..14e1b5a3f 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -27,7 +27,7 @@ require ( github.com/leaanthony/idgen v1.0.0 github.com/leaanthony/slicer v1.5.0 github.com/leaanthony/typescriptify-golang-structs v0.1.7 - github.com/leaanthony/winc v0.0.0-20220208061147-37b059b9dc3b + github.com/leaanthony/winc v0.0.0-20220323084916-ea5df694ec1f github.com/leaanthony/winicon v1.0.0 github.com/matryer/is v1.4.0 github.com/olekukonko/tablewriter v0.0.4 diff --git a/v2/go.sum b/v2/go.sum index 8e02f024a..ee11171e3 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -130,6 +130,8 @@ github.com/leaanthony/typescriptify-golang-structs v0.1.7 h1:yoznzWzyxkO/iWdlpq+ github.com/leaanthony/typescriptify-golang-structs v0.1.7/go.mod h1:cWtOkiVhMF77e6phAXUcfNwYmMwCJ67Sij24lfvi9Js= github.com/leaanthony/winc v0.0.0-20220208061147-37b059b9dc3b h1:cJ+VfVwX3GkRGSy0SiOyZ7FjSGMPAY/rS/wJzilo23I= github.com/leaanthony/winc v0.0.0-20220208061147-37b059b9dc3b/go.mod h1:OPfk8SNMAKRcSv8Vw1QL0yupmwcRtJyXZUgtMoaHUGc= +github.com/leaanthony/winc v0.0.0-20220323084916-ea5df694ec1f h1:RM0TNQXGTt06ZrSysdo+r9E9fk1ObACFBOww+W1zOiU= +github.com/leaanthony/winc v0.0.0-20220323084916-ea5df694ec1f/go.mod h1:OPfk8SNMAKRcSv8Vw1QL0yupmwcRtJyXZUgtMoaHUGc= github.com/leaanthony/winicon v1.0.0 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ= github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go index 58ab60df2..e8ddf4ab0 100644 --- a/v2/internal/frontend/desktop/windows/frontend.go +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" "log" "runtime" "strconv" @@ -45,10 +46,16 @@ type Frontend struct { servingFromDisk bool hasStarted bool + + // Windows build number + versionInfo *operatingsystem.WindowsVersionInfo } func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend { + // Get Windows build number + versionInfo, _ := operatingsystem.GetWindowsVersionInfo() + result := &Frontend{ frontendOptions: appoptions, logger: myLogger, @@ -56,6 +63,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. dispatcher: dispatcher, ctx: ctx, startURL: "http://wails.localhost/", + versionInfo: versionInfo, } bindingsJSON, err := appBindings.ToJSON() @@ -99,7 +107,7 @@ func (f *Frontend) Run(ctx context.Context) error { f.ctx = context.WithValue(ctx, "frontend", f) - mainWindow := NewWindow(nil, f.frontendOptions) + mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo) f.mainWindow = mainWindow var _debug = ctx.Value("debug") diff --git a/v2/internal/frontend/desktop/windows/menu.go b/v2/internal/frontend/desktop/windows/menu.go index c82581c10..d1a34fd1e 100644 --- a/v2/internal/frontend/desktop/windows/menu.go +++ b/v2/internal/frontend/desktop/windows/menu.go @@ -48,8 +48,10 @@ func processMenu(window *Window, menu *menu.Menu) { mainMenu := window.NewMenu() for _, menuItem := range menu.Items { submenu := mainMenu.AddSubMenu(menuItem.Label) - for _, menuItem := range menuItem.SubMenu.Items { - processMenuItem(submenu, menuItem) + if menuItem.SubMenu != nil { + for _, menuItem := range menuItem.SubMenu.Items { + processMenuItem(submenu, menuItem) + } } } mainMenu.Show() diff --git a/v2/internal/frontend/desktop/windows/theme.go b/v2/internal/frontend/desktop/windows/theme.go new file mode 100644 index 000000000..8da6e2d51 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/theme.go @@ -0,0 +1,62 @@ +package windows + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32" + "github.com/wailsapp/wails/v2/pkg/options/windows" +) + +func (w *Window) updateTheme() { + + if win32.IsCurrentlyHighContrastMode() { + return + } + + if !win32.SupportsThemes() { + return + } + // Only process if there's a theme change + isDarkMode := win32.IsCurrentlyDarkMode() + w.isDarkMode = isDarkMode + + // Default use system theme + winOptions := w.frontendOptions.Windows + var customTheme *windows.ThemeSettings + if winOptions != nil { + customTheme = winOptions.CustomTheme + if winOptions.Theme == windows.Dark { + isDarkMode = true + } + if winOptions.Theme == windows.Light { + isDarkMode = false + } + } + + win32.SetTheme(w.Handle(), isDarkMode) + + // Custom theme + if win32.SupportsCustomThemes() && customTheme != nil { + if w.isActive { + if isDarkMode { + println("1") + win32.SetTitleBarColour(w.Handle(), customTheme.DarkModeTitleBar) + win32.SetTitleTextColour(w.Handle(), customTheme.DarkModeTitleText) + win32.SetBorderColour(w.Handle(), customTheme.DarkModeBorder) + } else { + println("2") + win32.SetTitleBarColour(w.Handle(), customTheme.LightModeTitleBar) + win32.SetTitleTextColour(w.Handle(), customTheme.LightModeTitleText) + win32.SetBorderColour(w.Handle(), customTheme.LightModeBorder) + } + } else { + if isDarkMode { + win32.SetTitleBarColour(w.Handle(), customTheme.DarkModeTitleBarInactive) + win32.SetTitleTextColour(w.Handle(), customTheme.DarkModeTitleTextInactive) + win32.SetBorderColour(w.Handle(), customTheme.DarkModeBorderInactive) + } else { + win32.SetTitleBarColour(w.Handle(), customTheme.LightModeTitleBarInactive) + win32.SetTitleTextColour(w.Handle(), customTheme.LightModeTitleTextInactive) + win32.SetBorderColour(w.Handle(), customTheme.LightModeBorderInactive) + } + } + } +} diff --git a/v2/internal/frontend/desktop/windows/win32/consts.go b/v2/internal/frontend/desktop/windows/win32/consts.go new file mode 100644 index 000000000..b34cb65da --- /dev/null +++ b/v2/internal/frontend/desktop/windows/win32/consts.go @@ -0,0 +1,27 @@ +package win32 + +import ( + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" + "syscall" +) + +type HRESULT int32 +type HANDLE uintptr + +var ( + moduser32 = syscall.NewLazyDLL("user32.dll") + procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") + procGetWindowLong = moduser32.NewProc("GetWindowLongW") +) +var ( + moddwmapi = syscall.NewLazyDLL("dwmapi.dll") + procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute") + procDwmExtendFrameIntoClientArea = moddwmapi.NewProc("DwmExtendFrameIntoClientArea") +) +var windowsVersion, _ = operatingsystem.GetWindowsVersionInfo() + +func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool { + return windowsVersion.Major >= major && + windowsVersion.Minor >= minor && + windowsVersion.Build >= buildNumber +} diff --git a/v2/internal/frontend/desktop/windows/win32/theme.go b/v2/internal/frontend/desktop/windows/win32/theme.go new file mode 100644 index 000000000..c616b5c58 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/win32/theme.go @@ -0,0 +1,92 @@ +package win32 + +import ( + "golang.org/x/sys/windows/registry" + "unsafe" +) + +type DWMWINDOWATTRIBUTE int32 + +const DwmwaUseImmersiveDarkModeBefore20h1 DWMWINDOWATTRIBUTE = 19 +const DwmwaUseImmersiveDarkMode DWMWINDOWATTRIBUTE = 20 +const DwmwaBorderColor DWMWINDOWATTRIBUTE = 34 +const DwmwaCaptionColor DWMWINDOWATTRIBUTE = 35 +const DwmwaTextColor DWMWINDOWATTRIBUTE = 36 + +const SPI_GETHIGHCONTRAST = 0x0042 +const HCF_HIGHCONTRASTON = 0x00000001 + +func dwmSetWindowAttribute(hwnd uintptr, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) { + ret, _, err := procDwmSetWindowAttribute.Call( + hwnd, + uintptr(dwAttribute), + uintptr(pvAttribute), + cbAttribute) + if ret != 0 { + _ = err + // println(err.Error()) + } +} + +func SupportsThemes() bool { + // We can't support Windows versions before 17763 + return IsWindowsVersionAtLeast(10, 0, 17763) +} + +func SupportsCustomThemes() bool { + return IsWindowsVersionAtLeast(10, 0, 17763) +} + +func SetTheme(hwnd uintptr, useDarkMode bool) { + if IsWindowsVersionAtLeast(10, 0, 17763) { + attr := DwmwaUseImmersiveDarkModeBefore20h1 + if IsWindowsVersionAtLeast(10, 0, 18985) { + attr = DwmwaUseImmersiveDarkMode + } + dwmSetWindowAttribute(hwnd, attr, unsafe.Pointer(&useDarkMode), unsafe.Sizeof(&useDarkMode)) + } +} + +func SetTitleBarColour(hwnd uintptr, titleBarColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaCaptionColor, unsafe.Pointer(&titleBarColour), unsafe.Sizeof(titleBarColour)) +} + +func SetTitleTextColour(hwnd uintptr, titleTextColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaTextColor, unsafe.Pointer(&titleTextColour), unsafe.Sizeof(titleTextColour)) +} + +func SetBorderColour(hwnd uintptr, titleBorderColour int32) { + dwmSetWindowAttribute(hwnd, DwmwaBorderColor, unsafe.Pointer(&titleBorderColour), unsafe.Sizeof(titleBorderColour)) +} + +func IsCurrentlyDarkMode() bool { + key, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE) + if err != nil { + return false + } + defer key.Close() + + AppsUseLightTheme, _, err := key.GetIntegerValue("AppsUseLightTheme") + if err != nil { + return false + } + + return AppsUseLightTheme == 0 +} + +type highContrast struct { + CbSize uint32 + DwFlags uint32 + LpszDefaultScheme *int16 +} + +func IsCurrentlyHighContrastMode() bool { + var result highContrast + result.CbSize = uint32(unsafe.Sizeof(result)) + res, _, err := procSystemParametersInfo.Call(SPI_GETHIGHCONTRAST, uintptr(result.CbSize), uintptr(unsafe.Pointer(&result)), 0) + if res == 0 { + _ = err + return false + } + return result.DwFlags&HCF_HIGHCONTRASTON == HCF_HIGHCONTRASTON +} diff --git a/v2/internal/frontend/desktop/windows/win32/window.go b/v2/internal/frontend/desktop/windows/win32/window.go new file mode 100644 index 000000000..d18f2c807 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/win32/window.go @@ -0,0 +1,56 @@ +package win32 + +import ( + "fmt" + "log" + "syscall" + "unsafe" +) + +const ( + WS_MAXIMIZE = 0x01000000 + + GWL_STYLE = -16 +) + +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb773244.aspx +type MARGINS struct { + CxLeftWidth, CxRightWidth, CyTopHeight, CyBottomHeight int32 +} + +func ExtendFrameIntoClientArea(hwnd uintptr) { + // -1: Adds the default frame styling (aero shadow and e.g. rounded corners on Windows 11) + // Also shows the caption buttons if transparent ant translucent but they don't work. + // 0: Adds the default frame styling but no aero shadow, does not show the caption buttons. + // 1: Adds the default frame styling (aero shadow and e.g. rounded corners on Windows 11) but no caption buttons + // are shown if transparent ant translucent. + margins := &MARGINS{1, 1, 1, 1} // Only extend 1 pixel to have the default frame styling but no caption buttons + if err := dwmExtendFrameIntoClientArea(hwnd, margins); err != nil { + log.Fatal(fmt.Errorf("DwmExtendFrameIntoClientArea failed: %s", err)) + } +} + +func IsWindowMaximised(hwnd uintptr) bool { + style := uint32(getWindowLong(hwnd, GWL_STYLE)) + return style&WS_MAXIMIZE != 0 +} + +func dwmExtendFrameIntoClientArea(hwnd uintptr, margins *MARGINS) error { + ret, _, _ := procDwmExtendFrameIntoClientArea.Call( + hwnd, + uintptr(unsafe.Pointer(margins))) + + if ret != 0 { + return syscall.GetLastError() + } + + return nil +} + +func getWindowLong(hwnd uintptr, index int) int32 { + ret, _, _ := procGetWindowLong.Call( + hwnd, + uintptr(index)) + + return int32(ret) +} diff --git a/v2/internal/frontend/desktop/windows/window.go b/v2/internal/frontend/desktop/windows/window.go index 58f265c9a..3d7b3c244 100644 --- a/v2/internal/frontend/desktop/windows/window.go +++ b/v2/internal/frontend/desktop/windows/window.go @@ -3,9 +3,8 @@ package windows import ( - "fmt" - "log" - "syscall" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32" + "github.com/wailsapp/wails/v2/internal/system/operatingsystem" "unsafe" "github.com/leaanthony/winc" @@ -20,15 +19,20 @@ type Window struct { applicationMenu *menu.Menu notifyParentWindowPositionChanged func() error minWidth, minHeight, maxWidth, maxHeight int + versionInfo *operatingsystem.WindowsVersionInfo + isDarkMode bool + isActive bool } -func NewWindow(parent winc.Controller, appoptions *options.App) *Window { +func NewWindow(parent winc.Controller, appoptions *options.App, versionInfo *operatingsystem.WindowsVersionInfo) *Window { result := &Window{ frontendOptions: appoptions, minHeight: appoptions.MinHeight, minWidth: appoptions.MinWidth, maxHeight: appoptions.MaxHeight, maxWidth: appoptions.MaxWidth, + versionInfo: versionInfo, + isActive: true, } result.SetIsForm(true) @@ -46,7 +50,8 @@ func NewWindow(parent winc.Controller, appoptions *options.App) *Window { var dwStyle = w32.WS_OVERLAPPEDWINDOW winc.RegClassOnlyOnce("wailsWindow") - result.SetHandle(winc.CreateWindow("wailsWindow", parent, uint(exStyle), uint(dwStyle))) + handle := winc.CreateWindow("wailsWindow", parent, uint(exStyle), uint(dwStyle)) + result.SetHandle(handle) winc.RegMsgHandler(result) result.SetParent(parent) @@ -69,6 +74,8 @@ func NewWindow(parent winc.Controller, appoptions *options.App) *Window { result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight) } + result.updateTheme() + if appoptions.Windows != nil { if appoptions.Windows.WindowIsTranslucent { result.SetTranslucentBackground() @@ -125,12 +132,26 @@ func (w *Window) SetMaxSize(maxWidth int, maxHeight int) { func (w *Window) WndProc(msg uint32, wparam, lparam uintptr) uintptr { switch msg { + case w32.WM_SETTINGCHANGE: + settingChanged := w32.UTF16PtrToString((*uint16)(unsafe.Pointer(lparam))) + if settingChanged == "ImmersiveColorSet" { + w.updateTheme() + } + return 0 case w32.WM_NCLBUTTONDOWN: w32.SetFocus(w.Handle()) case w32.WM_MOVE, w32.WM_MOVING: if w.notifyParentWindowPositionChanged != nil { w.notifyParentWindowPositionChanged() } + case w32.WM_ACTIVATE: + if int(wparam) == w32.WA_INACTIVE { + w.isActive = false + w.updateTheme() + } else { + w.isActive = true + w.updateTheme() + } // TODO move WM_DPICHANGED handling into winc case 0x02E0: //w32.WM_DPICHANGED @@ -152,15 +173,7 @@ func (w *Window) WndProc(msg uint32, wparam, lparam uintptr) uintptr { // As a result we have hidden the titlebar but still have the default window frame styling. // See: https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmextendframeintoclientarea#remarks if winoptions := w.frontendOptions.Windows; winoptions == nil || !winoptions.DisableFramelessWindowDecorations { - // -1: Adds the default frame styling (aero shadow and e.g. rounded corners on Windows 11) - // Also shows the caption buttons if transparent ant translucent but they don't work. - // 0: Adds the default frame styling but no aero shadow, does not show the caption buttons. - // 1: Adds the default frame styling (aero shadow and e.g. rounded corners on Windows 11) but no caption buttons - // are shown if transparent ant translucent. - margins := w32.MARGINS{1, 1, 1, 1} // Only extend 1 pixel to have the default frame styling but no caption buttons - if err := dwmExtendFrameIntoClientArea(w.Handle(), margins); err != nil { - log.Fatal(fmt.Errorf("DwmExtendFrameIntoClientArea failed: %s", err)) - } + win32.ExtendFrameIntoClientArea(w.Handle()) } case w32.WM_NCCALCSIZE: // Disable the standard frame by allowing the client area to take the full @@ -208,25 +221,6 @@ func (w *Window) WndProc(msg uint32, wparam, lparam uintptr) uintptr { } func (w *Window) IsMaximised() bool { - style := uint32(w32.GetWindowLong(w.Handle(), w32.GWL_STYLE)) - return style&w32.WS_MAXIMIZE != 0 -} - -// TODO this should be put into the winc if we are happy with this solution. -var ( - modkernel32 = syscall.NewLazyDLL("dwmapi.dll") - - procDwmExtendFrameIntoClientArea = modkernel32.NewProc("DwmExtendFrameIntoClientArea") -) - -func dwmExtendFrameIntoClientArea(hwnd w32.HWND, margins w32.MARGINS) error { - ret, _, _ := procDwmExtendFrameIntoClientArea.Call( - uintptr(hwnd), - uintptr(unsafe.Pointer(&margins))) - - if ret != 0 { - return syscall.GetLastError() - } - - return nil + return win32.IsWindowMaximised(w.Handle()) + } diff --git a/v2/internal/system/operatingsystem/version_windows.go b/v2/internal/system/operatingsystem/version_windows.go new file mode 100644 index 000000000..766e8ff8b --- /dev/null +++ b/v2/internal/system/operatingsystem/version_windows.go @@ -0,0 +1,59 @@ +package operatingsystem + +import ( + "golang.org/x/sys/windows/registry" + "strconv" +) + +type WindowsVersionInfo struct { + Major int + Minor int + Build int + DisplayVersion string +} + +func (w *WindowsVersionInfo) IsWindowsVersionAtLeast(major, minor, buildNumber int) bool { + return w.Major >= major && w.Minor >= minor && w.Build >= buildNumber +} + +func GetWindowsVersionInfo() (*WindowsVersionInfo, error) { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + if err != nil { + return nil, err + } + + return &WindowsVersionInfo{ + Major: regDWORDKeyAsInt(key, "CurrentMajorVersionNumber"), + Minor: regDWORDKeyAsInt(key, "CurrentMinorVersionNumber"), + Build: regStringKeyAsInt(key, "CurrentBuildNumber"), + DisplayVersion: regKeyAsString(key, "DisplayVersion"), + }, nil +} + +func regDWORDKeyAsInt(key registry.Key, name string) int { + result, _, err := key.GetIntegerValue(name) + if err != nil { + return -1 + } + return int(result) +} + +func regStringKeyAsInt(key registry.Key, name string) int { + resultStr, _, err := key.GetStringValue(name) + if err != nil { + return -1 + } + result, err := strconv.Atoi(resultStr) + if err != nil { + return -1 + } + return result +} + +func regKeyAsString(key registry.Key, name string) string { + resultStr, _, err := key.GetStringValue(name) + if err != nil { + return "" + } + return resultStr +} diff --git a/v2/pkg/options/windows/windows.go b/v2/pkg/options/windows/windows.go index 99cd235ba..bf9934c95 100644 --- a/v2/pkg/options/windows/windows.go +++ b/v2/pkg/options/windows/windows.go @@ -1,5 +1,40 @@ package windows +type Theme int + +const ( + // SystemDefault will use whatever the system theme is. The application will follow system theme changes. + SystemDefault Theme = 0 + // Dark Mode + Dark Theme = 1 + // Light Mode + Light Theme = 2 +) + +func RGB(r, g, b uint8) int32 { + var col = int32(b) + col = col<<8 | int32(g) + col = col<<8 | int32(r) + return col +} + +// ThemeSettings contains optional colours to use. +// They may be set using the hex values: 0x00BBGGRR +type ThemeSettings struct { + DarkModeTitleBar int32 + DarkModeTitleBarInactive int32 + DarkModeTitleText int32 + DarkModeTitleTextInactive int32 + DarkModeBorder int32 + DarkModeBorderInactive int32 + LightModeTitleBar int32 + LightModeTitleBarInactive int32 + LightModeTitleText int32 + LightModeTitleTextInactive int32 + LightModeBorder int32 + LightModeBorderInactive int32 +} + // Options are options specific to Windows type Options struct { WebviewIsTransparent bool @@ -13,4 +48,10 @@ type Options struct { // Path where the WebView2 stores the user data. If empty %APPDATA%\[BinaryName.exe] will be used. // If the path is not valid, a messagebox will be displayed with the error and the app will exit with error code. WebviewUserDataPath string + + // Dark/Light or System Default Theme + Theme Theme + + // Custom settings for dark/light mode + CustomTheme *ThemeSettings } diff --git a/website/docs/reference/options.mdx b/website/docs/reference/options.mdx index 3440e3f00..529bbd2e9 100644 --- a/website/docs/reference/options.mdx +++ b/website/docs/reference/options.mdx @@ -47,6 +47,15 @@ func main() { DisableWindowIcon: false, DisableFramelessWindowDecorations: false, WebviewUserDataPath: "", + Theme: windows.SystemDefault, + CustomTheme: &windows.ThemeSettings{ + DarkModeTitleBar: windows.RGB(20, 20, 20), + DarkModeTitleText: windows.RGB(200, 200, 200), + DarkModeBorder: windows.RGB(20, 0, 20), + LightModeTitleBar: windows.RGB(200, 200, 200), + LightModeTitleText: windows.RGB(20, 20, 20), + LightModeBorder: windows.RGB(200, 200, 200), + }, }, Mac: &mac.Options{ TitleBar: &mac.TitleBar{ @@ -375,6 +384,78 @@ Type: string This defines the path where the WebView2 stores the user data. If empty `%APPDATA%\[BinaryName.exe]` will be used. +### Theme + +Name: Theme + +Type: `windows.Theme` + +Minimum Windows Version: Windows 10 2004/20H1 + +This defines the theme that the application should use: + +| Value | Description | +| --------------- | ----------- | +| SystemDefault | *Default*. The theme will be based on the system default. If the user changes their theme, the application will update to use the new setting | +| Dark | The application will use a dark theme exclusively | +| Light | The application will use a light theme exclusively | + + +### CustomTheme + +Name: CustomTheme + +Type: `windows.CustomTheme` + +Minimum Windows Version: Windows 10/11 2009/21H2 Build 22000 + +Allows you to specify custom colours for TitleBar, TitleText and Border for both light and dark mode, as well as +when the window is active or inactive. + +#### CustomTheme + +The CustomTheme struct uses `int32` to specify the colour values. These are in the standard(!) Windows format of: +`0x00BBGGAA`. A helper function is provided to do RGB conversions into this format: `windows.RGB(r,g,b uint8)`. + +NOTE: Any value not provided will default to black. + +```go +type ThemeSettings struct { + DarkModeTitleBar int32 + DarkModeTitleBarInactive int32 + DarkModeTitleText int32 + DarkModeTitleTextInactive int32 + DarkModeBorder int32 + DarkModeBorderInactive int32 + LightModeTitleBar int32 + LightModeTitleBarInactive int32 + LightModeTitleText int32 + LightModeTitleTextInactive int32 + LightModeBorder int32 + LightModeBorderInactive int32 +} +``` + +Example: +```go + CustomTheme: &windows.ThemeSettings{ + // Theme to use when window is active + DarkModeTitleBar: windows.RGB(255, 0, 0), // Red + DarkModeTitleText: windows.RGB(0, 255, 0), // Green + DarkModeBorder: windows.RGB(0, 0, 255), // Blue + LightModeTitleBar: windows.RGB(200, 200, 200), + LightModeTitleText: windows.RGB(20, 20, 20), + LightModeBorder: windows.RGB(200, 200, 200), + // Theme to use when window is inactive + DarkModeTitleBarInactive: windows.RGB(128, 0, 0), + DarkModeTitleTextInactive: windows.RGB(0, 128, 0), + DarkModeBorderInactive: windows.RGB(0, 0, 128), + LightModeTitleBarInactive: windows.RGB(100, 100, 100), + LightModeTitleTextInactive: windows.RGB(10, 10, 10), + LightModeBorderInactive: windows.RGB(100, 100, 100), + }, +``` + ## Mac Specific Options ### TitleBar