From 178ea9c8c52dc6880e62cae8195d2f465f1cb789 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 29 Apr 2023 12:14:12 +1000 Subject: [PATCH] [windows] Split out wndProc. Generate windows events, support per-window themes --- v3/internal/w32/user32.go | 6 ++ v3/pkg/application/application_windows.go | 59 +++++++++++--- v3/pkg/application/options_windows.go | 32 ++++++++ v3/pkg/application/webview_window_windows.go | 81 +++++++++++++++++++- v3/pkg/events/events.go | 12 +++ v3/pkg/events/events.txt | 2 +- v3/tasks/events/generate.go | 67 +++++++++++++++- 7 files changed, 242 insertions(+), 17 deletions(-) diff --git a/v3/internal/w32/user32.go b/v3/internal/w32/user32.go index 146472254..d337a6145 100644 --- a/v3/internal/w32/user32.go +++ b/v3/internal/w32/user32.go @@ -136,6 +136,7 @@ var ( procSetWindowsHookEx = moduser32.NewProc("SetWindowsHookExW") procUnhookWindowsHookEx = moduser32.NewProc("UnhookWindowsHookEx") procCallNextHookEx = moduser32.NewProc("CallNextHookEx") + procGetForegroundWindow = moduser32.NewProc("GetForegroundWindow") procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") procSetClassLong = moduser32.NewProc("SetClassLongW") @@ -301,6 +302,11 @@ func GetDpiForWindow(hwnd HWND) UINT { return uint(dpi) } +func GetForegroundWindow() HWND { + ret, _, _ := procGetForegroundWindow.Call() + return HWND(ret) +} + func SetWindowCompositionAttribute(hwnd HWND, data *WINDOWCOMPOSITIONATTRIBDATA) bool { if procSetWindowCompositionAttribute != nil { ret, _, _ := procSetWindowCompositionAttribute.Call( diff --git a/v3/pkg/application/application_windows.go b/v3/pkg/application/application_windows.go index 851a4e8f0..e24c0d154 100644 --- a/v3/pkg/application/application_windows.go +++ b/v3/pkg/application/application_windows.go @@ -3,6 +3,7 @@ package application import ( + "github.com/wailsapp/wails/v3/pkg/events" "syscall" "unsafe" @@ -17,8 +18,13 @@ type windowsApp struct { instance w32.HINSTANCE + windowMap map[w32.HWND]*windowsWebviewWindow + mainThreadID w32.HANDLE mainThreadWindowHWND w32.HWND + + // system theme + isDarkMode bool } func (m *windowsApp) getPrimaryScreen() (*Screen, error) { @@ -111,28 +117,57 @@ func (m *windowsApp) init() { if ret := w32.RegisterClassEx(&wc); ret == 0 { panic(syscall.GetLastError()) } + + m.isDarkMode = w32.IsCurrentlyDarkMode() } func (m *windowsApp) wndProc(hwnd w32.HWND, msg uint32, wParam, lParam uintptr) uintptr { - switch msg { - case w32.WM_SIZE: + + // Handle the invoke callback + if msg == wmInvokeCallback { + m.invokeCallback(wParam, lParam) return 0 - case w32.WM_CLOSE: - w32.PostQuitMessage(0) - return 0 - case wmInvokeCallback: - if hwnd == m.mainThreadWindowHWND { - m.invokeCallback(wParam, lParam) - return 0 - } } + switch msg { + case w32.WM_SETTINGCHANGE: + settingChanged := w32.UTF16PtrToString((*uint16)(unsafe.Pointer(lParam))) + if settingChanged == "ImmersiveColorSet" { + isDarkMode := w32.IsCurrentlyDarkMode() + if isDarkMode != m.isDarkMode { + applicationEvents <- uint(events.Windows.SystemThemeChanged) + m.isDarkMode = isDarkMode + } + } + return 0 + } + + if window, ok := m.windowMap[hwnd]; ok { + return window.WndProc(msg, wParam, lParam) + } + + // Dispatch the message to the appropriate window + return w32.DefWindowProc(hwnd, msg, wParam, lParam) } +func (m *windowsApp) registerWindow(result *windowsWebviewWindow) { + m.windowMap[result.hwnd] = result +} + +func (m *windowsApp) unregisterWindow(w *windowsWebviewWindow) { + delete(m.windowMap, w.hwnd) + + // If this was the last window... + if len(m.windowMap) == 0 { + w32.PostQuitMessage(0) + } +} + func newPlatformApp(app *App) *windowsApp { result := &windowsApp{ - parent: app, - instance: w32.GetModuleHandle(""), + parent: app, + instance: w32.GetModuleHandle(""), + windowMap: make(map[w32.HWND]*windowsWebviewWindow), } result.init() diff --git a/v3/pkg/application/options_windows.go b/v3/pkg/application/options_windows.go index 703b92601..55f69a7e2 100644 --- a/v3/pkg/application/options_windows.go +++ b/v3/pkg/application/options_windows.go @@ -15,4 +15,36 @@ type WindowsWindow struct { BackdropType BackdropType // Disable the icon in the titlebar DisableIcon bool + // Theme. Defaults to SystemDefault which will use whatever the system theme is. The application will follow system theme changes. + Theme Theme + // Custom colours for dark/light mode + CustomTheme *ThemeSettings +} + +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 +) + +// ThemeSettings defines custom colours to use in dark or light mode. +// 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 } diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 77136a49b..409964dd0 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "github.com/wailsapp/wails/v3/internal/w32" + "github.com/wailsapp/wails/v3/pkg/events" "syscall" "unsafe" @@ -107,6 +108,10 @@ func (w *windowsWebviewWindow) _run() { panic("Unable to create window") } + // Register the window with the application + windowsApp := globalApplication.impl.(*windowsApp) + windowsApp.registerWindow(w) + if options.DisableResize { w.setResizable(false) } @@ -129,12 +134,25 @@ func (w *windowsWebviewWindow) _run() { case BackgroundTypeTranslucent: w.setBackdropType(options.Windows.BackdropType) } + w.setForeground() + + // Process the theme + switch options.Windows.Theme { + case SystemDefault: + w.updateTheme(w32.IsCurrentlyDarkMode()) + globalApplication.On(events.Windows.SystemThemeChanged, func() { + w.updateTheme(w32.IsCurrentlyDarkMode()) + }) + case Light: + w.updateTheme(false) + case Dark: + w.updateTheme(true) + } if !options.Hidden { w.show() w.update() } - w.setForeground() } func (w *windowsWebviewWindow) center() { @@ -365,7 +383,7 @@ func (w *windowsWebviewWindow) setBackdropType(backdropType BackdropType) { w32.SetWindowCompositionAttribute(w.hwnd, &data) } else { backdropValue := backdropType - // We default to None, but in win32 None = 1 and Auto = 0 + // We default to None, but in w32 None = 1 and Auto = 0 // So we check if the value given was Auto and set it to 0 if backdropType == Auto { backdropValue = None @@ -393,6 +411,65 @@ func (w *windowsWebviewWindow) disableIcon() { ) } +func (w *windowsWebviewWindow) updateTheme(isDarkMode bool) { + + if w32.IsCurrentlyHighContrastMode() { + return + } + + if !w32.SupportsThemes() { + return + } + + w32.SetTheme(w.hwnd, isDarkMode) + + // Custom theme processing + customTheme := w.parent.options.Windows.CustomTheme + // Custom theme + if w32.SupportsCustomThemes() && customTheme != nil { + if w.isActive() { + if isDarkMode { + w32.SetTitleBarColour(w.hwnd, customTheme.DarkModeTitleBar) + w32.SetTitleTextColour(w.hwnd, customTheme.DarkModeTitleText) + w32.SetBorderColour(w.hwnd, customTheme.DarkModeBorder) + } else { + w32.SetTitleBarColour(w.hwnd, customTheme.LightModeTitleBar) + w32.SetTitleTextColour(w.hwnd, customTheme.LightModeTitleText) + w32.SetBorderColour(w.hwnd, customTheme.LightModeBorder) + } + } else { + if isDarkMode { + w32.SetTitleBarColour(w.hwnd, customTheme.DarkModeTitleBarInactive) + w32.SetTitleTextColour(w.hwnd, customTheme.DarkModeTitleTextInactive) + w32.SetBorderColour(w.hwnd, customTheme.DarkModeBorderInactive) + } else { + w32.SetTitleBarColour(w.hwnd, customTheme.LightModeTitleBarInactive) + w32.SetTitleTextColour(w.hwnd, customTheme.LightModeTitleTextInactive) + w32.SetBorderColour(w.hwnd, customTheme.LightModeBorderInactive) + } + } + } +} + +func (w *windowsWebviewWindow) isActive() bool { + return w32.GetForegroundWindow() == w.hwnd +} + +func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintptr { + switch msg { + case w32.WM_SIZE: + return 0 + case w32.WM_CLOSE: + w32.PostMessage(w.hwnd, w32.WM_QUIT, 0, 0) + // Unregister the window with the application + windowsApp := globalApplication.impl.(*windowsApp) + windowsApp.unregisterWindow(w) + return 0 + default: + return w32.DefWindowProc(w.hwnd, msg, wparam, lparam) + } +} + func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (w32.HICON, error) { var err error var result w32.HICON diff --git a/v3/pkg/events/events.go b/v3/pkg/events/events.go index f26f53b2e..23c0cf7b5 100644 --- a/v3/pkg/events/events.go +++ b/v3/pkg/events/events.go @@ -260,3 +260,15 @@ func newMacEvents() macEvents { WindowFileDraggingExited: 1145, } } + +var Windows = newWindowsEvents() + +type windowsEvents struct { + SystemThemeChanged ApplicationEventType +} + +func newWindowsEvents() windowsEvents { + return windowsEvents{ + SystemThemeChanged: 1146, + } +} diff --git a/v3/pkg/events/events.txt b/v3/pkg/events/events.txt index dbbe98db4..ae0338627 100644 --- a/v3/pkg/events/events.txt +++ b/v3/pkg/events/events.txt @@ -120,4 +120,4 @@ mac:WebViewDidCommitNavigation mac:WindowFileDraggingEntered mac:WindowFileDraggingPerformed mac:WindowFileDraggingExited - +windows:SystemThemeChanged \ No newline at end of file diff --git a/v3/tasks/events/generate.go b/v3/tasks/events/generate.go index eafa27f89..2c5869a1e 100644 --- a/v3/tasks/events/generate.go +++ b/v3/tasks/events/generate.go @@ -25,6 +25,18 @@ func newMacEvents() macEvents { return macEvents{ $$MACEVENTSVALUES } } + +var Windows = newWindowsEvents() + +type windowsEvents struct { +$$WINDOWSEVENTSDECL} + +func newWindowsEvents() windowsEvents { + return windowsEvents{ +$$WINDOWSEVENTSVALUES } +} + + ` var eventsH = `//go:build darwin @@ -53,7 +65,11 @@ func main() { applicationDelegateEvents := bytes.NewBufferString("") webviewDelegateEvents := bytes.NewBufferString("") + windowsEventsDecl := bytes.NewBufferString("") + windowsEventsValues := bytes.NewBufferString("") + var id int + var maxMacEvents int var line []byte // Loop over each line in the file for id, line = range bytes.Split(eventNames, []byte{'\n'}) { @@ -94,6 +110,7 @@ func main() { macEventsDecl.WriteString("\t" + eventTitle + " " + eventType + "\n") macEventsValues.WriteString("\t\t" + event + ": " + strconv.Itoa(id) + ",\n") cHeaderEvents.WriteString("#define Event" + eventTitle + " " + strconv.Itoa(id) + "\n") + maxMacEvents = id if ignoreEvent { continue } @@ -128,15 +145,61 @@ func main() { `) } - + case "windows": + eventType := "ApplicationEventType" + if strings.HasPrefix(event, "Window") { + eventType = "WindowEventType" + } + if strings.HasPrefix(event, "WebView") { + eventType = "WindowEventType" + } + windowsEventsDecl.WriteString("\t" + eventTitle + " " + eventType + "\n") + windowsEventsValues.WriteString("\t\t" + event + ": " + strconv.Itoa(id) + ",\n") + // cHeaderEvents.WriteString("#define Event" + eventTitle + " " + strconv.Itoa(id) + "\n") + // if ignoreEvent { + // continue + // } + // // Check if this is a window event + // if strings.HasPrefix(event, "Window") { + // windowDelegateEvents.WriteString(`- (void)` + delegateEventFunction + `:(NSNotification *)notification { + // if( hasListeners(Event` + eventTitle + `) ) { + // processWindowEvent(self.windowId, Event` + eventTitle + `); + // } + //} + // + //`) + // } + // // Check if this is a webview event + // if strings.HasPrefix(event, "WebView") { + // webViewFunction := strings.TrimPrefix(event, "WebView") + // webViewFunction = string(bytes.ToLower([]byte{webViewFunction[0]})) + webViewFunction[1:] + // webviewDelegateEvents.WriteString(`- (void)webView:(WKWebView *)webview ` + webViewFunction + `:(WKNavigation *)navigation { + // if( hasListeners(Event` + eventTitle + `) ) { + // processWindowEvent(self.windowId, Event` + eventTitle + `); + // } + //} + // + //`) + // } + // if strings.HasPrefix(event, "Application") { + // applicationDelegateEvents.WriteString(`- (void)` + delegateEventFunction + `:(NSNotification *)notification { + // if( hasListeners(Event` + eventTitle + `) ) { + // processApplicationEvent(Event` + eventTitle + `); + // } + //} + // + //`) + // } } } - cHeaderEvents.WriteString("\n#define MAX_EVENTS " + strconv.Itoa(id-1) + "\n") + cHeaderEvents.WriteString("\n#define MAX_EVENTS " + strconv.Itoa(maxMacEvents+1) + "\n") // Save the eventsGo template substituting the values and decls templateToWrite := strings.ReplaceAll(eventsGo, "$$MACEVENTSDECL", macEventsDecl.String()) templateToWrite = strings.ReplaceAll(templateToWrite, "$$MACEVENTSVALUES", macEventsValues.String()) + templateToWrite = strings.ReplaceAll(templateToWrite, "$$WINDOWSEVENTSDECL", windowsEventsDecl.String()) + templateToWrite = strings.ReplaceAll(templateToWrite, "$$WINDOWSEVENTSVALUES", windowsEventsValues.String()) err = os.WriteFile("../../pkg/events/events.go", []byte(templateToWrite), 0644) if err != nil { panic(err)