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:
39
src/main.cpp
39
src/main.cpp
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user