Skip to content

Commit b8b5da6

Browse files
committed
fix(decorated-window-jni): apply DPI scaling to window min/max size on Windows
On non-JBR JVMs (Azul, Oracle OpenJDK), AWT's WM_GETMINMAXINFO handler sets ptMinTrackSize in logical pixels but Windows expects physical pixels. This causes window.minimumSize to be ignored at HiDPI scaling levels. Intercept WM_GETMINMAXINFO in our WndProc subclass and apply MulDiv DPI scaling, matching the fix JBR applies via ScaleUpX/Y. Closes #102
1 parent 7650c1e commit b8b5da6

3 files changed

Lines changed: 128 additions & 0 deletions

File tree

decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/TitleBar.Windows.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ private fun DecoratedWindowScope.NativeWindowsTitleBar(
9999
}
100100
}
101101

102+
// Fix DPI scaling for min/max size on non-JBR JVMs (issue #102)
103+
SyncMinMaxSizeToNative(window)
104+
102105
val useNewFullscreenControls = modifier.hasNewFullscreenControls()
103106

104107
// ── Fullscreen with newFullscreenControls: sliding overlay ──
@@ -238,3 +241,38 @@ private fun DecoratedWindowScope.FallbackWindowsTitleBar(
238241
content(currentState)
239242
}
240243
}
244+
245+
/**
246+
* Syncs [java.awt.Window.minimumSize] and [java.awt.Window.maximumSize] to the native
247+
* WM_GETMINMAXINFO handler so that DPI scaling is applied correctly on non-JBR JVMs.
248+
*/
249+
@Suppress("FunctionNaming")
250+
@Composable
251+
private fun SyncMinMaxSizeToNative(window: java.awt.Window) {
252+
DisposableEffect(window) {
253+
val hwnd = JniWindowsWindowUtil.getHwnd(window)
254+
if (hwnd != 0L) {
255+
val syncSizes = {
256+
val min = window.minimumSize
257+
JniWindowsDecorationBridge.nativeSetMinimumSize(hwnd, min.width, min.height)
258+
val max = window.maximumSize
259+
val maxW = if (max.width < Short.MAX_VALUE) max.width else 0
260+
val maxH = if (max.height < Short.MAX_VALUE) max.height else 0
261+
JniWindowsDecorationBridge.nativeSetMaximumSize(hwnd, maxW, maxH)
262+
}
263+
syncSizes()
264+
val propertyListener =
265+
java.beans.PropertyChangeListener { evt ->
266+
if (evt.propertyName == "minimumSize" || evt.propertyName == "maximumSize") {
267+
syncSizes()
268+
}
269+
}
270+
window.addPropertyChangeListener(propertyListener)
271+
onDispose {
272+
window.removePropertyChangeListener(propertyListener)
273+
}
274+
} else {
275+
onDispose { }
276+
}
277+
}
278+
}

decorated-window-jni/src/main/kotlin/io/github/kdroidfilter/nucleus/window/utils/windows/JniWindowsDecorationBridge.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,26 @@ internal object JniWindowsDecorationBridge {
7373
argb: Int,
7474
)
7575

76+
// Sets the minimum window size in logical pixels. The native
77+
// WM_GETMINMAXINFO handler applies DPI scaling automatically.
78+
// Pass (0, 0) to disable the override and fall back to AWT default.
79+
@JvmStatic
80+
external fun nativeSetMinimumSize(
81+
hwnd: Long,
82+
widthPx: Int,
83+
heightPx: Int,
84+
)
85+
86+
// Sets the maximum window size in logical pixels. The native
87+
// WM_GETMINMAXINFO handler applies DPI scaling automatically.
88+
// Pass (0, 0) to disable the override and fall back to AWT default.
89+
@JvmStatic
90+
external fun nativeSetMaximumSize(
91+
hwnd: Long,
92+
widthPx: Int,
93+
heightPx: Int,
94+
)
95+
7696
// Returns debug counters as a string (temporary).
7797
@JvmStatic
7898
external fun nativeGetDebugInfo(hwnd: Long): String

decorated-window-jni/src/main/native/windows/nucleus_windows_decoration.c

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ typedef struct {
9393
LONG savedStyle;
9494
LONG savedExStyle;
9595
WINDOWPLACEMENT savedPlacement;
96+
/* Min/max size override (logical pixels, 0 = not set) */
97+
POINT minSizePx;
98+
POINT maxSizePx;
9699
/* Debug counters */
97100
int hitTestCount;
98101
int hitTestCaption;
@@ -418,6 +421,33 @@ static LRESULT CALLBACK decorationWndProc(
418421
break; /* also let original handle it */
419422
}
420423

424+
/* -------------------------------------------------------------- */
425+
/* WM_GETMINMAXINFO: fix DPI scaling for min/max size. */
426+
/* Standard OpenJDK stores logical pixels in ptMinTrackSize but */
427+
/* Windows expects physical pixels. JBR applies ScaleUpX/Y; */
428+
/* we replicate that fix here so it works on all JVMs. */
429+
/* -------------------------------------------------------------- */
430+
case WM_GETMINMAXINFO: {
431+
/* Let AWT process first (sets unscaled values) */
432+
LRESULT result = CallWindowProcW(state->originalWndProc,
433+
hwnd, msg, wParam, lParam);
434+
LPMINMAXINFO lpmmi = (LPMINMAXINFO)lParam;
435+
UINT dpi = getDpi(hwnd);
436+
437+
/* Override with DPI-scaled values if set */
438+
if (state->minSizePx.x > 0 || state->minSizePx.y > 0) {
439+
lpmmi->ptMinTrackSize.x = MulDiv(state->minSizePx.x, dpi, 96);
440+
lpmmi->ptMinTrackSize.y = MulDiv(state->minSizePx.y, dpi, 96);
441+
}
442+
if (state->maxSizePx.x > 0 || state->maxSizePx.y > 0) {
443+
if (state->maxSizePx.x > 0)
444+
lpmmi->ptMaxTrackSize.x = MulDiv(state->maxSizePx.x, dpi, 96);
445+
if (state->maxSizePx.y > 0)
446+
lpmmi->ptMaxTrackSize.y = MulDiv(state->maxSizePx.y, dpi, 96);
447+
}
448+
return result;
449+
}
450+
421451
/* -------------------------------------------------------------- */
422452
/* WM_SYSCOMMAND: block state-changing commands while fullscreen */
423453
/* to prevent native/Kotlin state desync. The application must */
@@ -1032,3 +1062,43 @@ Java_io_github_kdroidfilter_nucleus_window_utils_windows_JniWindowsDecorationBri
10321062
}
10331063
return (*env)->NewStringUTF(env, buf);
10341064
}
1065+
1066+
/* -------------------------------------------------------------- */
1067+
/* nativeSetMinimumSize(long hwnd, int widthPx, int heightPx) */
1068+
/* Stores the minimum window size in logical pixels. The */
1069+
/* WM_GETMINMAXINFO handler applies DPI scaling automatically. */
1070+
/* Pass (0, 0) to disable the override and fall back to AWT. */
1071+
/* -------------------------------------------------------------- */
1072+
JNIEXPORT void JNICALL
1073+
Java_io_github_kdroidfilter_nucleus_window_utils_windows_JniWindowsDecorationBridge_nativeSetMinimumSize(
1074+
JNIEnv *env, jclass clazz, jlong hwndLong, jint widthPx, jint heightPx)
1075+
{
1076+
HWND hwnd = (HWND)(uintptr_t)hwndLong;
1077+
if (!hwnd) return;
1078+
1079+
DecoState *state = getState(hwnd);
1080+
if (!state) return;
1081+
1082+
state->minSizePx.x = (LONG)widthPx;
1083+
state->minSizePx.y = (LONG)heightPx;
1084+
}
1085+
1086+
/* -------------------------------------------------------------- */
1087+
/* nativeSetMaximumSize(long hwnd, int widthPx, int heightPx) */
1088+
/* Stores the maximum window size in logical pixels. The */
1089+
/* WM_GETMINMAXINFO handler applies DPI scaling automatically. */
1090+
/* Pass (0, 0) to disable the override and fall back to AWT. */
1091+
/* -------------------------------------------------------------- */
1092+
JNIEXPORT void JNICALL
1093+
Java_io_github_kdroidfilter_nucleus_window_utils_windows_JniWindowsDecorationBridge_nativeSetMaximumSize(
1094+
JNIEnv *env, jclass clazz, jlong hwndLong, jint widthPx, jint heightPx)
1095+
{
1096+
HWND hwnd = (HWND)(uintptr_t)hwndLong;
1097+
if (!hwnd) return;
1098+
1099+
DecoState *state = getState(hwnd);
1100+
if (!state) return;
1101+
1102+
state->maxSizePx.x = (LONG)widthPx;
1103+
state->maxSizePx.y = (LONG)heightPx;
1104+
}

0 commit comments

Comments
 (0)