Skip to content

Commit edff91e

Browse files
committed
fix(decorated-window-jni): per-axis min/max DPI scaling, cleanup on dispose, GraalVM metadata
- Handle each min/max axis independently in WM_GETMINMAXINFO - Reset native min/max overrides on Compose dispose - Add nativeSetMinimumSize/nativeSetMaximumSize to reachability-metadata.json - Add minimum size demo to example app - Document DPI-aware min/max size fix in decorated-window docs
1 parent b8b5da6 commit edff91e

5 files changed

Lines changed: 43 additions & 9 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ private fun SyncMinMaxSizeToNative(window: java.awt.Window) {
270270
window.addPropertyChangeListener(propertyListener)
271271
onDispose {
272272
window.removePropertyChangeListener(propertyListener)
273+
JniWindowsDecorationBridge.nativeSetMinimumSize(hwnd, 0, 0)
274+
JniWindowsDecorationBridge.nativeSetMaximumSize(hwnd, 0, 0)
273275
}
274276
} else {
275277
onDispose { }

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -434,17 +434,15 @@ static LRESULT CALLBACK decorationWndProc(
434434
LPMINMAXINFO lpmmi = (LPMINMAXINFO)lParam;
435435
UINT dpi = getDpi(hwnd);
436436

437-
/* Override with DPI-scaled values if set */
438-
if (state->minSizePx.x > 0 || state->minSizePx.y > 0) {
437+
/* Override with DPI-scaled values if set (per-axis) */
438+
if (state->minSizePx.x > 0)
439439
lpmmi->ptMinTrackSize.x = MulDiv(state->minSizePx.x, dpi, 96);
440+
if (state->minSizePx.y > 0)
440441
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-
}
442+
if (state->maxSizePx.x > 0)
443+
lpmmi->ptMaxTrackSize.x = MulDiv(state->maxSizePx.x, dpi, 96);
444+
if (state->maxSizePx.y > 0)
445+
lpmmi->ptMaxTrackSize.y = MulDiv(state->maxSizePx.y, dpi, 96);
448446
return result;
449447
}
450448

decorated-window-jni/src/main/resources/META-INF/native-image/io.github.kdroidfilter/nucleus.decorated-window-jni/reachability-metadata.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,28 @@
1212
]
1313
}
1414
]
15+
},
16+
{
17+
"type": "io.github.kdroidfilter.nucleus.window.utils.windows.JniWindowsDecorationBridge",
18+
"jniAccessible": true,
19+
"methods": [
20+
{
21+
"name": "nativeSetMinimumSize",
22+
"parameterTypes": [
23+
"long",
24+
"int",
25+
"int"
26+
]
27+
},
28+
{
29+
"name": "nativeSetMaximumSize",
30+
"parameterTypes": [
31+
"long",
32+
"int",
33+
"int"
34+
]
35+
}
36+
]
1537
}
1638
]
1739
}

docs/runtime/decorated-window.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ This module does not depend on JBR, making it compatible with **any JVM** (OpenJ
5858
!!! info "macOS: Liquid Glass and Xcode 26 appearance"
5959
Nucleus automatically patches the application launcher's `LC_BUILD_VERSION` to macOS SDK 26.0 via `vtool`, enabling Liquid Glass window decorations (larger traffic lights, rounded corners). This works with **any JDK** — a JDK compiled with Xcode 26 is no longer required. See [macOS 26 Window Appearance](../targets/macos.md#macos-26-window-appearance-liquid-glass) for details and configuration options.
6060

61+
!!! note "Windows: DPI-aware minimum and maximum window size"
62+
On non-JBR JVMs (OpenJDK, GraalVM), `Window.minimumSize` and `Window.maximumSize` are stored in logical pixels but Windows expects physical pixels in `WM_GETMINMAXINFO`. This causes the enforced min/max size to be too small on HiDPI displays (e.g. a 640×480 minimum becomes 427×320 at 150% scaling). JBR fixes this internally with `ScaleUpX`/`ScaleUpY`.
63+
64+
The JNI module replicates this fix: it intercepts `WM_GETMINMAXINFO` after AWT and applies `MulDiv(value, dpi, 96)` scaling. Just set `window.minimumSize` or `window.maximumSize` as usual — the DPI correction is automatic.
65+
66+
This fix is **not present** in `decorated-window-jbr` (JBR handles it natively).
67+
6168
!!! note "Windows: no white background flash during resize"
6269
On Windows, Skiko's rendering pipeline clears the DirectX canvas to white before each frame. When the window is resized larger, the newly exposed pixels remain white for one frame — producing a visible white flash. The JNI module eliminates this by adjusting Skiko's clear color to transparent for dark themes (rendered as opaque black on the DirectX surface), so the flash is invisible against a dark background. It also synchronizes the DWM caption and border colors (`DWMWA_CAPTION_COLOR`, `DWMWA_BORDER_COLOR`, `DWMWA_USE_IMMERSIVE_DARK_MODE`) with the title bar color for consistent Windows 11 window chrome styling.
6370

@@ -175,6 +182,7 @@ The following tables compare a standard Compose `Window()`, the JBR module (`dec
175182
| True fullscreen | Broken (doesn't cover taskbar) | Broken (doesn't cover taskbar) | **Fixed** — native Win32 fullscreen (`newFullscreenControls()`) |
176183
| Fullscreen sliding title bar | No | No | Yes (`newFullscreenControls()`) |
177184
| DWM dark mode sync | No | No | Yes (`DWMWA_USE_IMMERSIVE_DARK_MODE`, caption/border color) |
185+
| DPI-aware min/max size | Broken on non-JBR | JBR handles it | **Fixed**`WM_GETMINMAXINFO` DPI scaling |
178186
| RTL support | No custom title bar | Yes (no hot-swap, restart required) | Yes (live hot-swap) |
179187
| JDK requirement | Any | JBR only | Any |
180188
| Fallback (no native lib) | N/A | N/A | Compose `windowDragHandler()` (no WndProc subclass) |

example/src/main/kotlin/com/example/demo/Main.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ fun main(args: Array<String>) {
175175
onCloseRequest = ::exitApplication,
176176
title = "Nucleus Demo",
177177
) {
178+
// Set minimum window size (DPI-scaled automatically by JNI module)
179+
LaunchedEffect(Unit) {
180+
window.minimumSize = java.awt.Dimension(640, 480)
181+
}
178182
CompositionLocalProvider(
179183
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
180184
) {

0 commit comments

Comments
 (0)