mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 19:50:15 +08:00
[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
This commit is contained in:
parent
2e21f25182
commit
48254b73e5
@ -27,7 +27,7 @@ require (
|
|||||||
github.com/leaanthony/idgen v1.0.0
|
github.com/leaanthony/idgen v1.0.0
|
||||||
github.com/leaanthony/slicer v1.5.0
|
github.com/leaanthony/slicer v1.5.0
|
||||||
github.com/leaanthony/typescriptify-golang-structs v0.1.7
|
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/leaanthony/winicon v1.0.0
|
||||||
github.com/matryer/is v1.4.0
|
github.com/matryer/is v1.4.0
|
||||||
github.com/olekukonko/tablewriter v0.0.4
|
github.com/olekukonko/tablewriter v0.0.4
|
||||||
|
@ -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/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 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-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 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ=
|
||||||
github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU=
|
github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU=
|
||||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/wailsapp/wails/v2/internal/system/operatingsystem"
|
||||||
"log"
|
"log"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -45,10 +46,16 @@ type Frontend struct {
|
|||||||
servingFromDisk bool
|
servingFromDisk bool
|
||||||
|
|
||||||
hasStarted 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 {
|
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{
|
result := &Frontend{
|
||||||
frontendOptions: appoptions,
|
frontendOptions: appoptions,
|
||||||
logger: myLogger,
|
logger: myLogger,
|
||||||
@ -56,6 +63,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
|||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
startURL: "http://wails.localhost/",
|
startURL: "http://wails.localhost/",
|
||||||
|
versionInfo: versionInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
bindingsJSON, err := appBindings.ToJSON()
|
bindingsJSON, err := appBindings.ToJSON()
|
||||||
@ -99,7 +107,7 @@ func (f *Frontend) Run(ctx context.Context) error {
|
|||||||
|
|
||||||
f.ctx = context.WithValue(ctx, "frontend", f)
|
f.ctx = context.WithValue(ctx, "frontend", f)
|
||||||
|
|
||||||
mainWindow := NewWindow(nil, f.frontendOptions)
|
mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo)
|
||||||
f.mainWindow = mainWindow
|
f.mainWindow = mainWindow
|
||||||
|
|
||||||
var _debug = ctx.Value("debug")
|
var _debug = ctx.Value("debug")
|
||||||
|
@ -48,8 +48,10 @@ func processMenu(window *Window, menu *menu.Menu) {
|
|||||||
mainMenu := window.NewMenu()
|
mainMenu := window.NewMenu()
|
||||||
for _, menuItem := range menu.Items {
|
for _, menuItem := range menu.Items {
|
||||||
submenu := mainMenu.AddSubMenu(menuItem.Label)
|
submenu := mainMenu.AddSubMenu(menuItem.Label)
|
||||||
for _, menuItem := range menuItem.SubMenu.Items {
|
if menuItem.SubMenu != nil {
|
||||||
processMenuItem(submenu, menuItem)
|
for _, menuItem := range menuItem.SubMenu.Items {
|
||||||
|
processMenuItem(submenu, menuItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mainMenu.Show()
|
mainMenu.Show()
|
||||||
|
62
v2/internal/frontend/desktop/windows/theme.go
Normal file
62
v2/internal/frontend/desktop/windows/theme.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
v2/internal/frontend/desktop/windows/win32/consts.go
Normal file
27
v2/internal/frontend/desktop/windows/win32/consts.go
Normal file
@ -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
|
||||||
|
}
|
92
v2/internal/frontend/desktop/windows/win32/theme.go
Normal file
92
v2/internal/frontend/desktop/windows/win32/theme.go
Normal file
@ -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
|
||||||
|
}
|
56
v2/internal/frontend/desktop/windows/win32/window.go
Normal file
56
v2/internal/frontend/desktop/windows/win32/window.go
Normal file
@ -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)
|
||||||
|
}
|
@ -3,9 +3,8 @@
|
|||||||
package windows
|
package windows
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32"
|
||||||
"log"
|
"github.com/wailsapp/wails/v2/internal/system/operatingsystem"
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/leaanthony/winc"
|
"github.com/leaanthony/winc"
|
||||||
@ -20,15 +19,20 @@ type Window struct {
|
|||||||
applicationMenu *menu.Menu
|
applicationMenu *menu.Menu
|
||||||
notifyParentWindowPositionChanged func() error
|
notifyParentWindowPositionChanged func() error
|
||||||
minWidth, minHeight, maxWidth, maxHeight int
|
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{
|
result := &Window{
|
||||||
frontendOptions: appoptions,
|
frontendOptions: appoptions,
|
||||||
minHeight: appoptions.MinHeight,
|
minHeight: appoptions.MinHeight,
|
||||||
minWidth: appoptions.MinWidth,
|
minWidth: appoptions.MinWidth,
|
||||||
maxHeight: appoptions.MaxHeight,
|
maxHeight: appoptions.MaxHeight,
|
||||||
maxWidth: appoptions.MaxWidth,
|
maxWidth: appoptions.MaxWidth,
|
||||||
|
versionInfo: versionInfo,
|
||||||
|
isActive: true,
|
||||||
}
|
}
|
||||||
result.SetIsForm(true)
|
result.SetIsForm(true)
|
||||||
|
|
||||||
@ -46,7 +50,8 @@ func NewWindow(parent winc.Controller, appoptions *options.App) *Window {
|
|||||||
var dwStyle = w32.WS_OVERLAPPEDWINDOW
|
var dwStyle = w32.WS_OVERLAPPEDWINDOW
|
||||||
|
|
||||||
winc.RegClassOnlyOnce("wailsWindow")
|
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)
|
winc.RegMsgHandler(result)
|
||||||
result.SetParent(parent)
|
result.SetParent(parent)
|
||||||
|
|
||||||
@ -69,6 +74,8 @@ func NewWindow(parent winc.Controller, appoptions *options.App) *Window {
|
|||||||
result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight)
|
result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.updateTheme()
|
||||||
|
|
||||||
if appoptions.Windows != nil {
|
if appoptions.Windows != nil {
|
||||||
if appoptions.Windows.WindowIsTranslucent {
|
if appoptions.Windows.WindowIsTranslucent {
|
||||||
result.SetTranslucentBackground()
|
result.SetTranslucentBackground()
|
||||||
@ -125,12 +132,26 @@ func (w *Window) SetMaxSize(maxWidth int, maxHeight int) {
|
|||||||
func (w *Window) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
|
func (w *Window) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
|
||||||
|
|
||||||
switch msg {
|
switch msg {
|
||||||
|
case w32.WM_SETTINGCHANGE:
|
||||||
|
settingChanged := w32.UTF16PtrToString((*uint16)(unsafe.Pointer(lparam)))
|
||||||
|
if settingChanged == "ImmersiveColorSet" {
|
||||||
|
w.updateTheme()
|
||||||
|
}
|
||||||
|
return 0
|
||||||
case w32.WM_NCLBUTTONDOWN:
|
case w32.WM_NCLBUTTONDOWN:
|
||||||
w32.SetFocus(w.Handle())
|
w32.SetFocus(w.Handle())
|
||||||
case w32.WM_MOVE, w32.WM_MOVING:
|
case w32.WM_MOVE, w32.WM_MOVING:
|
||||||
if w.notifyParentWindowPositionChanged != nil {
|
if w.notifyParentWindowPositionChanged != nil {
|
||||||
w.notifyParentWindowPositionChanged()
|
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
|
// TODO move WM_DPICHANGED handling into winc
|
||||||
case 0x02E0: //w32.WM_DPICHANGED
|
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.
|
// 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
|
// See: https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmextendframeintoclientarea#remarks
|
||||||
if winoptions := w.frontendOptions.Windows; winoptions == nil || !winoptions.DisableFramelessWindowDecorations {
|
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)
|
win32.ExtendFrameIntoClientArea(w.Handle())
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case w32.WM_NCCALCSIZE:
|
case w32.WM_NCCALCSIZE:
|
||||||
// Disable the standard frame by allowing the client area to take the full
|
// 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 {
|
func (w *Window) IsMaximised() bool {
|
||||||
style := uint32(w32.GetWindowLong(w.Handle(), w32.GWL_STYLE))
|
return win32.IsWindowMaximised(w.Handle())
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
59
v2/internal/system/operatingsystem/version_windows.go
Normal file
59
v2/internal/system/operatingsystem/version_windows.go
Normal file
@ -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
|
||||||
|
}
|
@ -1,5 +1,40 @@
|
|||||||
package windows
|
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
|
// Options are options specific to Windows
|
||||||
type Options struct {
|
type Options struct {
|
||||||
WebviewIsTransparent bool
|
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.
|
// 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.
|
// If the path is not valid, a messagebox will be displayed with the error and the app will exit with error code.
|
||||||
WebviewUserDataPath string
|
WebviewUserDataPath string
|
||||||
|
|
||||||
|
// Dark/Light or System Default Theme
|
||||||
|
Theme Theme
|
||||||
|
|
||||||
|
// Custom settings for dark/light mode
|
||||||
|
CustomTheme *ThemeSettings
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,15 @@ func main() {
|
|||||||
DisableWindowIcon: false,
|
DisableWindowIcon: false,
|
||||||
DisableFramelessWindowDecorations: false,
|
DisableFramelessWindowDecorations: false,
|
||||||
WebviewUserDataPath: "",
|
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{
|
Mac: &mac.Options{
|
||||||
TitleBar: &mac.TitleBar{
|
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.
|
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
|
## Mac Specific Options
|
||||||
|
|
||||||
### TitleBar
|
### TitleBar
|
||||||
|
Loading…
Reference in New Issue
Block a user