diff --git a/v3/examples/plain/main.go b/v3/examples/plain/main.go index bc8406e82..63ba8d88b 100644 --- a/v3/examples/plain/main.go +++ b/v3/examples/plain/main.go @@ -4,6 +4,7 @@ import ( _ "embed" "log" "net/http" + "time" "github.com/wailsapp/wails/v3/pkg/application" ) @@ -38,6 +39,21 @@ func main() { println("clicked") }) + go func() { + time.Sleep(5 * time.Second) + + app.NewWebviewWindowWithOptions(&application.WebviewWindowOptions{ + Title: "Plain Bundle new Window from GoRoutine", + Width: 500, + Height: 500, + Mac: application.MacWindow{ + Backdrop: application.MacBackdropTranslucent, + TitleBar: application.MacTitleBarHiddenInsetUnified, + InvisibleTitleBarHeight: 50, + }, + }) + }() + err := app.Run() if err != nil { diff --git a/v3/internal/w32/functions.go b/v3/internal/w32/functions.go index d521a3397..d8ca0c43a 100644 --- a/v3/internal/w32/functions.go +++ b/v3/internal/w32/functions.go @@ -225,6 +225,7 @@ var ( destroyCursor = user32.NewProc("DestroyCursor") getDlgCtrlID = user32.NewProc("GetDlgCtrlID") systemParametersInfo = user32.NewProc("SystemParametersInfoW") + registerWindowMessage = user32.NewProc("RegisterWindowMessageW") regCreateKeyEx = advapi32.NewProc("RegCreateKeyExW") regOpenKeyEx = advapi32.NewProc("RegOpenKeyExW") @@ -364,6 +365,7 @@ var ( mulDiv = kernel32.NewProc("MulDiv") getConsoleWindow = kernel32.NewProc("GetConsoleWindow") getCurrentThread = kernel32.NewProc("GetCurrentThread") + getCurrentThreadId = kernel32.NewProc("GetCurrentThreadId") getLogicalDrives = kernel32.NewProc("GetLogicalDrives") getDriveType = kernel32.NewProc("GetDriveTypeW") getUserDefaultLCID = kernel32.NewProc("GetUserDefaultLCID") @@ -482,6 +484,14 @@ var ( setProcessDpiAwareness = shcore.NewProc("SetProcessDpiAwareness") ) +func RegisterWindowMessage(name string) uint32 { + ret, _, _ := registerWindowMessage.Call( + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name))), + ) + + return uint32(ret) +} + // RegisterClassEx sets the Size of the WNDCLASSEX automatically. func RegisterClassEx(wndClassEx *WNDCLASSEX) ATOM { if wndClassEx != nil { @@ -3674,6 +3684,11 @@ func GetCurrentThread() HANDLE { return HANDLE(ret) } +func GetCurrentThreadId() HANDLE { + ret, _, _ := getCurrentThreadId.Call() + return HANDLE(ret) +} + func GetLogicalDrives() uint32 { ret, _, _ := getLogicalDrives.Call() return uint32(ret) diff --git a/v3/pkg/application/application_windows.go b/v3/pkg/application/application_windows.go index 156414579..851a4e8f0 100644 --- a/v3/pkg/application/application_windows.go +++ b/v3/pkg/application/application_windows.go @@ -3,10 +3,11 @@ package application import ( - "github.com/samber/lo" - "github.com/wailsapp/wails/v3/internal/w32" "syscall" "unsafe" + + "github.com/samber/lo" + "github.com/wailsapp/wails/v3/internal/w32" ) var windowClassName = lo.Must(syscall.UTF16PtrFromString("WailsWebviewWindow")) @@ -15,11 +16,9 @@ type windowsApp struct { parent *App instance w32.HINSTANCE -} -func (m *windowsApp) dispatchOnMainThread(id uint) { - //TODO implement me - panic("implement me") + mainThreadID w32.HANDLE + mainThreadWindowHWND w32.HWND } func (m *windowsApp) getPrimaryScreen() (*Screen, error) { @@ -116,27 +115,20 @@ func (m *windowsApp) init() { func (m *windowsApp) wndProc(hwnd w32.HWND, msg uint32, wParam, lParam uintptr) uintptr { switch msg { - case w32.WM_SIZE, w32.WM_PAINT: + case w32.WM_SIZE: return 0 case w32.WM_CLOSE: w32.PostQuitMessage(0) return 0 + case wmInvokeCallback: + if hwnd == m.mainThreadWindowHWND { + m.invokeCallback(wParam, lParam) + return 0 + } } return w32.DefWindowProc(hwnd, msg, wParam, lParam) } -func (m *windowsApp) runMainLoop() int { - msg := (*w32.MSG)(unsafe.Pointer(w32.GlobalAlloc(0, uint32(unsafe.Sizeof(w32.MSG{}))))) - defer w32.GlobalFree(w32.HGLOBAL(unsafe.Pointer(m))) - - for w32.GetMessage(msg, 0, 0, 0) != 0 { - w32.TranslateMessage(msg) - w32.DispatchMessage(msg) - } - - return int(msg.WParam) -} - func newPlatformApp(app *App) *windowsApp { result := &windowsApp{ parent: app, @@ -144,6 +136,7 @@ func newPlatformApp(app *App) *windowsApp { } result.init() + result.initMainLoop() return result } diff --git a/v3/pkg/application/mainthread_windows.go b/v3/pkg/application/mainthread_windows.go new file mode 100644 index 000000000..313848254 --- /dev/null +++ b/v3/pkg/application/mainthread_windows.go @@ -0,0 +1,129 @@ +//go:build windows + +package application + +import ( + "runtime" + "sort" + "syscall" + "unsafe" + + "github.com/samber/lo" + "github.com/wailsapp/wails/v3/internal/w32" +) + +var ( + wmInvokeCallback uint32 +) + +func init() { + wmInvokeCallback = w32.RegisterWindowMessage("WailsV0.InvokeCallback") +} + +// initMainLoop must be called with the same OSThread that is used to call runMainLoop() later. +func (m *windowsApp) initMainLoop() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if m.mainThreadWindowHWND != 0 { + panic("initMainLoop was already called") + } + + // We need a hidden window so we can PostMessage to it, if we don't use PostMessage for dispatching to a HWND + // messages might get lost if a modal inner loop is being run. + // We had this once in V2: https://github.com/wailsapp/wails/issues/969 + // See: https://devblogs.microsoft.com/oldnewthing/20050426-18/?p=35783 + // See also: https://learn.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues#creating-a-message-loop + // > Because the system directs messages to individual windows in an application, a thread must create at least one window before starting its message loop. + m.mainThreadWindowHWND = w32.CreateWindowEx( + 0, + windowClassName, + lo.Must(syscall.UTF16PtrFromString("__wails_hidden_mainthread")), + w32.WS_DISABLED, + w32.CW_USEDEFAULT, + w32.CW_USEDEFAULT, + 0, + 0, + 0, + 0, + w32.GetModuleHandle(""), + nil) + + m.mainThreadID, _ = w32.GetWindowThreadProcessId(m.mainThreadWindowHWND) +} + +func (m *windowsApp) runMainLoop() int { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if m.invokeRequired() { + panic("invokeRequired for runMainLoop, the mainloop must be running on the same OSThread as the mainThreadWindow has been created on") + } + + msg := (*w32.MSG)(unsafe.Pointer(w32.GlobalAlloc(0, uint32(unsafe.Sizeof(w32.MSG{}))))) + defer w32.GlobalFree(w32.HGLOBAL(unsafe.Pointer(m))) + + for w32.GetMessage(msg, 0, 0, 0) != 0 { + w32.TranslateMessage(msg) + w32.DispatchMessage(msg) + } + + return int(msg.WParam) +} + +func (m *windowsApp) dispatchOnMainThread(id uint) { + mainThreadHWND := m.mainThreadWindowHWND + if mainThreadHWND == 0 { + panic("initMainLoop was not called") + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if m.invokeRequired() { + w32.PostMessage(mainThreadHWND, wmInvokeCallback, uintptr(id), 0) + } else { + mainThreadFunctionStoreLock.Lock() + fn := mainThreadFunctionStore[id] + delete(mainThreadFunctionStore, id) + mainThreadFunctionStoreLock.Unlock() + + if fn == nil { + Fatal("dispatchOnMainThread called with invalid id: %v", id) + } + fn() + } +} + +func (m *windowsApp) invokeRequired() bool { + mainThreadID := m.mainThreadID + if mainThreadID == 0 { + panic("initMainLoop was not called") + } + + return mainThreadID != w32.GetCurrentThreadId() +} + +func (m *windowsApp) invokeCallback(wParam, lParam uintptr) { + // TODO: Should we invoke just one or all queued? In v2 we always invoked all pendings... + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if m.invokeRequired() { + panic("invokeCallback must always be called on the MainOSThread") + } + + mainThreadFunctionStoreLock.Lock() + fnIDs := make([]uint, 0, len(mainThreadFunctionStore)) + for id := range mainThreadFunctionStore { + fnIDs = append(fnIDs, id) + } + sort.Slice(fnIDs, func(i, j int) bool { return fnIDs[i] < fnIDs[j] }) + + fns := make([]func(), len(fnIDs)) + for i, id := range fnIDs { + fns[i] = mainThreadFunctionStore[id] + delete(mainThreadFunctionStore, id) + } + mainThreadFunctionStoreLock.Unlock() + + for _, fn := range fns { + fn() + } +} diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 6e536ca50..25c8f8b18 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -3,10 +3,11 @@ package application import ( - "github.com/samber/lo" - "github.com/wailsapp/wails/v3/internal/w32" "syscall" "unsafe" + + "github.com/samber/lo" + "github.com/wailsapp/wails/v3/internal/w32" ) var showDevTools = func(window unsafe.Pointer) {} @@ -67,7 +68,10 @@ func (w *windowsWebviewWindow) setBackgroundColour(color *RGBA) { } func (w *windowsWebviewWindow) run() { + globalApplication.dispatchOnMainThread(w._run) +} +func (w *windowsWebviewWindow) _run() { var exStyle uint options := w.parent.options windowsOptions := options.Windows @@ -84,7 +88,7 @@ func (w *windowsWebviewWindow) run() { hwnd = w32.CreateWindowEx( exStyle, windowClassName, - lo.Must(syscall.UTF16PtrFromString("My Window Title")), + lo.Must(syscall.UTF16PtrFromString(options.Title)), w32.WS_OVERLAPPEDWINDOW|w32.WS_VISIBLE, w32.CW_USEDEFAULT, w32.CW_USEDEFAULT,