From 8cbd0cab9d24de9685b396fceaf9ba787c7bd9cf Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 9 Jul 2026 23:58:05 -0500 Subject: [PATCH] fix(window): preserve the exact minimum height across monitor DPI moves Moving the window between monitors of different scaling (150% -> 100% -> 150%) left the height off the minimum in BOTH directions, forcing a manual resize each time. A deep trace against the vendored SDL3 source found two distinct causes: - Shrink leg (higher -> lower scale): handleDisplayScaleChange called SDL_SetWindowSize BEFORE SDL_SetWindowMinimumSize. SDL clamps a size request up to the current minimum (SDL_video.c:3184-3191), so the smaller target was clamped back to the stale larger min; SetWindowMinimumSize only grows a window, never shrinks it, so lowering the min afterwards didn't pull it back. The deferred retry masked it only on rendered frames, so it leaked when the app was idle. Fix: set the new minimum BEFORE resizing. - Return leg (lower -> higher scale): the single-shot dpiResizePending flag let a later settling WINDOW_RESIZED in the same burst be misclassified as a user resize and recorded, corrupting savedSizeForScale for that scale so the remembered min was lost. Fix: a frame-counted settle window (dpiSettleFrames) suppresses resize-recording for the whole transition burst regardless of event ordering; also clamp any restored/scaled size up to the new scale's minimum. Only user resizes outside the ~40-frame settle burst are recorded, so normal resizing is unaffected. SDL clamp semantics verified in-repo; build clean. The transition needs real multi-monitor hardware to exercise end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main.cpp | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index b9c2082..38481cc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -539,7 +539,8 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale, std::map>& savedSizeForScale, int& lastKnownW, int& lastKnownH, int& dpiTargetW, int& dpiTargetH, - int& dpiResizeRetries, bool& dpiResizePending) + int& dpiResizeRetries, bool& dpiResizePending, + int& dpiSettleFrames) { auto& typo = dragonx::ui::material::Typography::instance(); float oldScale = typo.getDpiScale(); @@ -581,6 +582,12 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale, newH = (int)lroundf((float)lastKnownH * newScale / oldScale); } + // Never restore/scale below the new scale's minimum — guarantees the window lands exactly + // on the min in both directions even if the per-scale map was ever seeded oddly. + const int minW = (int)(1024 * newScale), minH = (int)(720 * newScale); + newW = std::max(newW, minW); + newH = std::max(newH, minH); + // Clamp to the target display's work area so the // window doesn't overflow a smaller/higher-density monitor. SDL_DisplayID did = SDL_GetDisplayForWindow(window); @@ -592,18 +599,24 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale, } } + // Set the new minimum BEFORE resizing. SDL_SetWindowSize clamps the request UP to the + // current minimum, so when moving to a LOWER scale the shrink (e.g. 1080 -> 720) issued + // while the min is still the old 1080 would be clamped back to 1080 and stick there + // (SDL_SetWindowMinimumSize only ever grows a window, never shrinks it). Min-then-size + // makes the shrink land on the new min immediately, without relying on the deferred retry. + SDL_SetWindowMinimumSize(window, minW, minH); dpiResizePending = true; SDL_SetWindowSize(window, newW, newH); lastKnownW = newW; lastKnownH = newH; - // Arm deferred retry — the SetWindowSize above may - // not stick if the user is mid-drag (modal loop). + // Arm deferred retry (SetWindowSize may not stick mid-drag) and suppress resize-recording + // for the whole settle burst: a transient WINDOW_RESIZED (ours, the WM_DPICHANGED settle, + // or a retry frame) that arrives after dpiResizePending was consumed and with scales already + // matching would otherwise be misclassified as a user resize and corrupt savedSizeForScale. dpiTargetW = newW; dpiTargetH = newH; dpiResizeRetries = 30; // retry for ~30 frames - SDL_SetWindowMinimumSize(window, - (int)(1024 * newScale), - (int)(720 * newScale)); + dpiSettleFrames = 40; // ignore resize-recording until the transition settles DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n", newW, newH, newScale, newPct); @@ -1277,6 +1290,9 @@ int main(int argc, char* argv[]) // overrides SetWindowPos). We retry for several frames. int dpiTargetW = 0, dpiTargetH = 0; int dpiResizeRetries = 0; + // Frames to keep ignoring resize-recording after a DPI transition (settle window) so transient + // WINDOW_RESIZED events during the transition can't corrupt savedSizeForScale / lastKnownW/H. + int dpiSettleFrames = 0; { float s = dragonx::ui::material::Typography::instance().getDpiScale(); int w = 0, h = 0; @@ -1306,6 +1322,9 @@ int main(int argc, char* argv[]) --dpiResizeRetries; needsRedraw = true; } + // Burn down the post-DPI-transition settle window (see handleDisplayScaleChange). The retry + // above forces redraws while active, so this drains promptly even if the app is otherwise idle. + if (dpiSettleFrames > 0) --dpiSettleFrames; // P6: Idle rendering — sleep if nothing needs redrawing if (!needsRedraw) { @@ -1336,7 +1355,7 @@ int main(int argc, char* argv[]) #ifdef __APPLE__ actualScale = storedScale; // macOS Retina: scales always match (both 1.0) #endif - bool isDpiResize = dpiResizePending || + bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending || std::abs(actualScale - storedScale) > 0.01f; if (dpiResizePending) dpiResizePending = false; if (!isDpiResize) { @@ -1361,7 +1380,7 @@ int main(int argc, char* argv[]) #endif handleDisplayScaleChange(window, newScale, savedSizeForScale, lastKnownW, lastKnownH, dpiTargetW, dpiTargetH, - dpiResizeRetries, dpiResizePending); + dpiResizeRetries, dpiResizePending, dpiSettleFrames); } needsRedraw = true; // Got an event — redraw } @@ -1439,7 +1458,7 @@ int main(int argc, char* argv[]) #ifdef __APPLE__ actualScale = storedScale; // macOS Retina: scales always match (both 1.0) #endif - bool isDpiResize = dpiResizePending || + bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending || std::abs(actualScale - storedScale) > 0.01f; if (dpiResizePending) dpiResizePending = false; bool isFSResize = fontScaleResizePending; @@ -1474,7 +1493,7 @@ int main(int argc, char* argv[]) #endif handleDisplayScaleChange(window, newScale, savedSizeForScale, lastKnownW, lastKnownH, dpiTargetW, dpiTargetH, - dpiResizeRetries, dpiResizePending); + dpiResizeRetries, dpiResizePending, dpiSettleFrames); } }