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) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 23:58:05 -05:00
parent a9f0d544cc
commit 8cbd0cab9d

View File

@@ -539,7 +539,8 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
std::map<int,std::pair<int,int>>& 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);
}
}