diff --git a/v3/pkg/application/webview_window_modal_windows_test.go b/v3/pkg/application/webview_window_modal_windows_test.go new file mode 100644 index 00000000000..7a6b4a5357f --- /dev/null +++ b/v3/pkg/application/webview_window_modal_windows_test.go @@ -0,0 +1,72 @@ +//go:build windows && !server + +package application + +import ( + "os" + "strings" + "testing" +) + +// Regression test for https://github.com/wailsapp/wails/issues/6054. +func TestModalParentIsReleasedBeforeWindowDestruction(t *testing.T) { + sourceBytes, err := os.ReadFile("webview_window_windows.go") + if err != nil { + t.Fatal(err) + } + source := string(sourceBytes) + + assertCallOrder( + t, + source, + "func (w *windowsWebviewWindow) destroy()", + "func (w *windowsWebviewWindow) reload()", + "w.releaseModalParent(w.isFocused())", + "w32.DestroyWindow(w.hwnd)", + ) + assertCallOrder( + t, + source, + "case w32.WM_CLOSE:", + "case w32.WM_SETCURSOR:", + "w.releaseModalParent(restoreParentActivation)", + "w32.DefWindowProc(w.hwnd, w32.WM_CLOSE, 0, 0)", + ) +} + +func TestModalParentActivationIsConditional(t *testing.T) { + sourceBytes, err := os.ReadFile("webview_window_windows.go") + if err != nil { + t.Fatal(err) + } + assertCallOrder( + t, + string(sourceBytes), + "func (w *windowsWebviewWindow) releaseModalParent(activate bool)", + "func (w *windowsWebviewWindow) reload()", + "if activate {", + "w32.SetActiveWindow(w.parentHWND)", + ) +} + +func assertCallOrder(t *testing.T, source, start, end, first, second string) { + t.Helper() + startIndex := strings.Index(source, start) + if startIndex < 0 { + t.Fatalf("start marker %q not found", start) + } + section := source[startIndex:] + endIndex := strings.Index(section, end) + if endIndex < 0 { + t.Fatalf("end marker %q not found after %q", end, start) + } + section = section[:endIndex] + firstIndex := strings.Index(section, first) + secondIndex := strings.Index(section, second) + if firstIndex < 0 || secondIndex < 0 { + t.Fatalf("expected %q and %q between %q and %q", first, second, start, end) + } + if firstIndex > secondIndex { + t.Fatalf("%q must occur before %q between %q and %q", first, second, start, end) + } +} diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index b397a95697d..6d9c97d4ed6 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -854,17 +854,28 @@ func (w *windowsWebviewWindow) setRelativePosition(x int, y int) { } func (w *windowsWebviewWindow) destroy() { - // Re-enable parent window if this was a modal window - if w.parentHWND != 0 { - w32.EnableWindow(w.parentHWND, true) - w.parentHWND = 0 - } + w.releaseModalParent(w.isFocused()) w.parent.markAsDestroyed() // destroy the window w32.DestroyWindow(w.hwnd) } +// releaseModalParent makes an attached modal's owner eligible for activation +// again. If the modal is currently foreground, activate its owner before the +// modal is destroyed so Windows does not select the next unrelated top-level +// window. A background modal must not steal activation from another app. +func (w *windowsWebviewWindow) releaseModalParent(activate bool) { + if w.parentHWND == 0 { + return + } + w32.EnableWindow(w.parentHWND, true) + if activate { + w32.SetActiveWindow(w.parentHWND) + } + w.parentHWND = 0 +} + func (w *windowsWebviewWindow) reload() { w.execJS("window.location.reload();") } @@ -1645,21 +1656,15 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp return 0 } - defer func() { - // Re-enable parent window if this was a modal window - if w.parentHWND != 0 { - w32.EnableWindow(w.parentHWND, true) - w.parentHWND = 0 - } - - windowsApp := globalApplication.impl.(*windowsApp) - windowsApp.unregisterWindow(w) - - }() - - // Now do the actual close + // DefWindowProc destroys the modal and may select the next unrelated + // top-level window, so remember whether its owner should remain active. + restoreParentActivation := w.isFocused() w.chromium.ShuttingDown() - return w32.DefWindowProc(w.hwnd, w32.WM_CLOSE, 0, 0) + w.releaseModalParent(restoreParentActivation) + result := w32.DefWindowProc(w.hwnd, w32.WM_CLOSE, 0, 0) + windowsApp := globalApplication.impl.(*windowsApp) + windowsApp.unregisterWindow(w) + return result case w32.WM_SETCURSOR: if w.compositionCursor != 0 && w32.LOWORD(uint32(lparam)) == w32.HTCLIENT { w32.SetCursor(w.compositionCursor)