From f2bf43a8bc39530e5c7d0cbc1d06bd400bb540b1 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 23 Aug 2026 10:57:42 +0200 Subject: [PATCH] fix(windows): restore window placement onto a live monitor without a blank flash The remembered window placement restored invisible when its monitor was gone (undocked laptop, powered-off TV), briefly flashed a blank window at the restored spot before the first Flutter frame, and could lose the maximized state: the exit path hides the window before a multi-second teardown, so a debounced save landing in that gap recorded SW_HIDE, and a window closed while minimized-from-maximized restored as a normal window. Validate the saved rect against current monitors and keep only the size on a miss, apply the placement with SW_HIDE so the first-frame callback stays the single show, skip persisting while the window is hidden, and honor WPF_RESTORETOMAXIMIZED when deciding to relaunch maximized. --- windows/runner/flutter_window.cpp | 43 ++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 519650177..05cd9799e 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -35,12 +35,19 @@ static void WriteWindowPlacement(const WINDOWPLACEMENT& wp) { } static void SaveWindowPlacement(HWND hwnd) { + // Never persist a hidden window: the exit path hides the window before its + // multi-second teardown, and a save landing in that gap would record + // SW_HIDE over the user's real show state. + if (!IsWindowVisible(hwnd)) return; WINDOWPLACEMENT wp{}; wp.length = sizeof(wp); if (!GetWindowPlacement(hwnd, &wp)) return; WriteWindowPlacement(wp); } +// Loads the saved WINDOWPLACEMENT and applies it while keeping the window +// hidden; the first-frame callback in OnCreate performs the single show. +// Returns whether the window should be shown maximized. static bool LoadWindowPlacement(HWND hwnd) { HKEY hKey; if (RegOpenKeyExW(HKEY_CURRENT_USER, kWindowPlacementKey, 0, KEY_READ, &hKey) != ERROR_SUCCESS) return false; @@ -53,10 +60,34 @@ static bool LoadWindowPlacement(HWND hwnd) { if (RegQueryValueExW(hKey, kWindowPlacementValue, nullptr, nullptr, reinterpret_cast(&wp), &size) == ERROR_SUCCESS && size == sizeof(wp)) { - // Prevent restoring as minimized - if (wp.showCmd == SW_SHOWMINIMIZED) wp.showCmd = SW_SHOWNORMAL; + // A window minimized away from the maximized state stores SW_SHOWMINIMIZED + // plus WPF_RESTORETOMAXIMIZED; both spellings mean "maximized" on relaunch. + wasMaximized = wp.showCmd == SW_SHOWMAXIMIZED || (wp.flags & WPF_RESTORETOMAXIMIZED) != 0; + + // The saved monitor may be gone (undocked laptop, powered-off TV). + // rcNormalPosition is in workspace coordinates — screen coordinates offset + // by the primary work area — so convert before testing against monitors. + // On a miss, keep the size but fall back to the default creation position + // so the window never restores invisible. + RECT workArea{}; + SystemParametersInfoW(SPI_GETWORKAREA, 0, &workArea, 0); + RECT screenRect = wp.rcNormalPosition; + OffsetRect(&screenRect, workArea.left, workArea.top); + if (MonitorFromRect(&screenRect, MONITOR_DEFAULTTONULL) == nullptr) { + RECT current{}; + GetWindowRect(hwnd, ¤t); + const LONG width = wp.rcNormalPosition.right - wp.rcNormalPosition.left; + const LONG height = wp.rcNormalPosition.bottom - wp.rcNormalPosition.top; + wp.rcNormalPosition.left = current.left - workArea.left; + wp.rcNormalPosition.top = current.top - workArea.top; + wp.rcNormalPosition.right = wp.rcNormalPosition.left + width; + wp.rcNormalPosition.bottom = wp.rcNormalPosition.top + height; + } + + // Apply hidden. A visible showCmd here would display a blank window at + // the restored spot before Flutter has rendered anything. + wp.showCmd = SW_HIDE; SetWindowPlacement(hwnd, &wp); - wasMaximized = (wp.showCmd == SW_SHOWMAXIMIZED); } RegCloseKey(hKey); @@ -155,9 +186,9 @@ FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam flutter_controller_->engine()->ReloadSystemFonts(); break; case WM_WINDOWPOSCHANGED: - // Don't persist placement while in fullscreen or mid-toggle — the rect - // would overwrite the user's real window position. - if (!is_fullscreen_ && !g_suppressPlacementSave) { + // Don't persist placement while fullscreen, mid-toggle, or hidden — the + // rect would overwrite the user's real window position or show state. + if (!is_fullscreen_ && !g_suppressPlacementSave && IsWindowVisible(hwnd)) { DebounceSaveWindowPlacement(hwnd); } break;