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,
|
std::map<int,std::pair<int,int>>& savedSizeForScale,
|
||||||
int& lastKnownW, int& lastKnownH,
|
int& lastKnownW, int& lastKnownH,
|
||||||
int& dpiTargetW, int& dpiTargetH,
|
int& dpiTargetW, int& dpiTargetH,
|
||||||
int& dpiResizeRetries, bool& dpiResizePending)
|
int& dpiResizeRetries, bool& dpiResizePending,
|
||||||
|
int& dpiSettleFrames)
|
||||||
{
|
{
|
||||||
auto& typo = dragonx::ui::material::Typography::instance();
|
auto& typo = dragonx::ui::material::Typography::instance();
|
||||||
float oldScale = typo.getDpiScale();
|
float oldScale = typo.getDpiScale();
|
||||||
@@ -581,6 +582,12 @@ static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
|||||||
newH = (int)lroundf((float)lastKnownH * newScale / oldScale);
|
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
|
// Clamp to the target display's work area so the
|
||||||
// window doesn't overflow a smaller/higher-density monitor.
|
// window doesn't overflow a smaller/higher-density monitor.
|
||||||
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
|
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;
|
dpiResizePending = true;
|
||||||
SDL_SetWindowSize(window, newW, newH);
|
SDL_SetWindowSize(window, newW, newH);
|
||||||
lastKnownW = newW;
|
lastKnownW = newW;
|
||||||
lastKnownH = newH;
|
lastKnownH = newH;
|
||||||
// Arm deferred retry — the SetWindowSize above may
|
// Arm deferred retry (SetWindowSize may not stick mid-drag) and suppress resize-recording
|
||||||
// not stick if the user is mid-drag (modal loop).
|
// 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;
|
dpiTargetW = newW;
|
||||||
dpiTargetH = newH;
|
dpiTargetH = newH;
|
||||||
dpiResizeRetries = 30; // retry for ~30 frames
|
dpiResizeRetries = 30; // retry for ~30 frames
|
||||||
SDL_SetWindowMinimumSize(window,
|
dpiSettleFrames = 40; // ignore resize-recording until the transition settles
|
||||||
(int)(1024 * newScale),
|
|
||||||
(int)(720 * newScale));
|
|
||||||
DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n",
|
DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n",
|
||||||
newW, newH, newScale, newPct);
|
newW, newH, newScale, newPct);
|
||||||
|
|
||||||
@@ -1277,6 +1290,9 @@ int main(int argc, char* argv[])
|
|||||||
// overrides SetWindowPos). We retry for several frames.
|
// overrides SetWindowPos). We retry for several frames.
|
||||||
int dpiTargetW = 0, dpiTargetH = 0;
|
int dpiTargetW = 0, dpiTargetH = 0;
|
||||||
int dpiResizeRetries = 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();
|
float s = dragonx::ui::material::Typography::instance().getDpiScale();
|
||||||
int w = 0, h = 0;
|
int w = 0, h = 0;
|
||||||
@@ -1306,6 +1322,9 @@ int main(int argc, char* argv[])
|
|||||||
--dpiResizeRetries;
|
--dpiResizeRetries;
|
||||||
needsRedraw = true;
|
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
|
// P6: Idle rendering — sleep if nothing needs redrawing
|
||||||
if (!needsRedraw) {
|
if (!needsRedraw) {
|
||||||
@@ -1336,7 +1355,7 @@ int main(int argc, char* argv[])
|
|||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
|
actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
|
||||||
#endif
|
#endif
|
||||||
bool isDpiResize = dpiResizePending ||
|
bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending ||
|
||||||
std::abs(actualScale - storedScale) > 0.01f;
|
std::abs(actualScale - storedScale) > 0.01f;
|
||||||
if (dpiResizePending) dpiResizePending = false;
|
if (dpiResizePending) dpiResizePending = false;
|
||||||
if (!isDpiResize) {
|
if (!isDpiResize) {
|
||||||
@@ -1361,7 +1380,7 @@ int main(int argc, char* argv[])
|
|||||||
#endif
|
#endif
|
||||||
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
||||||
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
||||||
dpiResizeRetries, dpiResizePending);
|
dpiResizeRetries, dpiResizePending, dpiSettleFrames);
|
||||||
}
|
}
|
||||||
needsRedraw = true; // Got an event — redraw
|
needsRedraw = true; // Got an event — redraw
|
||||||
}
|
}
|
||||||
@@ -1439,7 +1458,7 @@ int main(int argc, char* argv[])
|
|||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
|
actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
|
||||||
#endif
|
#endif
|
||||||
bool isDpiResize = dpiResizePending ||
|
bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending ||
|
||||||
std::abs(actualScale - storedScale) > 0.01f;
|
std::abs(actualScale - storedScale) > 0.01f;
|
||||||
if (dpiResizePending) dpiResizePending = false;
|
if (dpiResizePending) dpiResizePending = false;
|
||||||
bool isFSResize = fontScaleResizePending;
|
bool isFSResize = fontScaleResizePending;
|
||||||
@@ -1474,7 +1493,7 @@ int main(int argc, char* argv[])
|
|||||||
#endif
|
#endif
|
||||||
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
||||||
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
||||||
dpiResizeRetries, dpiResizePending);
|
dpiResizeRetries, dpiResizePending, dpiSettleFrames);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user