Skip to content

Commit 910879d

Browse files
committed
fix(decorated-window-tao): restore focus and popup rendering on Windows
Replace SetCapture-based focus trapping with thread-local WH_MOUSE hook that observes clicks without consuming them, allowing the parent window to maintain focus while popups remain interactive. Fixes: popups losing input focus when appearing, clicks not reaching parent/popup content. Add multi-host GL state management: force resetGLAll() every frame when multiple TaoComposeSceneHostWindows coexist (main + DecoratedDialog) to prevent Skia DirectContext state cache drift from sibling HGLRC swaps. Replace AWT-based monitor work area detection with JNI bridge to avoid Java2D/D3D initialization on Tao UI thread, which blocked WGL context. Extended workAreaSize constraint to popup scenes for proper layout when popup content exceeds parent window bounds. - Removed SetCapture() mechanism; replaced with refcounted WH_MOUSE hook - Changed nucleus_tao_windows_popup.c PopupState.captureHeld → outsideMonitorActive - Added TaoComposeSceneHostWindows.attachedHostCount: AtomicInteger - Added TaoPopupHostWindows.workAreaSize property with JNI implementation - Added TaoPopupSceneLayer(Windows).sceneLayoutSize computed from workAreaSize - Added NativeViewOverlayControllerWindows.workAreaSize forwarding
1 parent 2950272 commit 910879d

7 files changed

Lines changed: 199 additions & 72 deletions

File tree

decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/NativeViewOverlayControllerWindows.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ internal class NativeViewOverlayControllerWindows(
117117
override val parentHwnd: Long get() = popupHost.parentHwnd
118118
override val scale: Float get() = popupHost.scale
119119
override val parentWindowSize: IntSize get() = popupHost.parentWindowSize
120+
override val workAreaSize: IntSize get() = popupHost.workAreaSize
120121
override val sceneCoroutineContext: CoroutineContext get() = popupHost.sceneCoroutineContext
121122
override val hostDirectContext: DirectContext get() = popupHost.hostDirectContext
122123
override val coordinateOffset: IntOffset

decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/render/TaoComposeSceneHostWindows.kt

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ internal class TaoComposeSceneHostWindows(
170170
attachmentHandle = handle
171171

172172
directContext = DirectContext.makeGL()
173+
attachedHostCount.incrementAndGet()
173174

174175
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
175176
val dndManager =
@@ -645,7 +646,16 @@ internal class TaoComposeSceneHostWindows(
645646
// (state-cache invalidation only); calling it on every frame
646647
// unconditionally is too heavy for some drivers (nvoglv64 chokes),
647648
// so we gate on the flag.
648-
if (hostContextDirtied) {
649+
// Sibling-host mode: another TaoComposeSceneHostWindows is alive
650+
// (e.g., DecoratedDialog over a DecoratedWindow). Each host owns
651+
// its own HGLRC + DirectContext, and the dialog's onRedrawRequested
652+
// can run between our frames — swapping the current WGL context
653+
// behind our back. Our DirectContext's per-context GL state cache
654+
// is then stale relative to GL, and the next flushAndSubmit
655+
// reaches a NULL pointer inside the driver. Force resetGLAll on
656+
// every frame entry while >1 host coexists; revert to the
657+
// popup-only flag-gated path once it's just us.
658+
if (hostContextDirtied || attachedHostCount.get() > 1) {
649659
ctx.resetGLAll()
650660
hostContextDirtied = false
651661
}
@@ -841,6 +851,23 @@ internal class TaoComposeSceneHostWindows(
841851
override val parentHwnd: Long get() = outer.hwnd
842852
override val scale: Float get() = outer.scale
843853
override val parentWindowSize: IntSize get() = IntSize(outer.widthPx, outer.heightPx)
854+
override val workAreaSize: IntSize get() {
855+
// Use the primary monitor's work area resolved via the
856+
// existing JNI bridge — avoids touching AWT
857+
// (GraphicsEnvironment.getLocalGraphicsEnvironment) on the
858+
// Tao UI thread, which on Windows can lazily initialise
859+
// Java2D's D3D pipeline and conflict with the WGL context
860+
// bound to this thread (manifested as a hang + crash when
861+
// a second host attached, e.g. on DecoratedDialog open).
862+
if (!NativeTaoWindowsDecoBridge.isLoaded) return parentWindowSize
863+
val area =
864+
NativeTaoWindowsDecoBridge.nativeGetPrimaryMonitorWorkArea()
865+
?: return parentWindowSize
866+
if (area.size < 4) return parentWindowSize
867+
val w = area[2].toInt().coerceAtLeast(1)
868+
val h = area[3].toInt().coerceAtLeast(1)
869+
return IntSize(w, h)
870+
}
844871
override val sceneCoroutineContext: kotlin.coroutines.CoroutineContext
845872
get() = outer.coroutineContext + outer.frameClock + outer.flushingDispatcher
846873
override val hostDirectContext: DirectContext get() = ctx
@@ -991,8 +1018,11 @@ internal class TaoComposeSceneHostWindows(
9911018
fun detach() {
9921019
scene?.close()
9931020
scene = null
994-
directContext?.close()
995-
directContext = null
1021+
if (directContext != null) {
1022+
directContext?.close()
1023+
directContext = null
1024+
attachedHostCount.decrementAndGet()
1025+
}
9961026
if (attachmentHandle != 0L) {
9971027
NativeTaoGlBridge.nativeDetach(attachmentHandle)
9981028
attachmentHandle = 0L
@@ -1014,6 +1044,23 @@ internal class TaoComposeSceneHostWindows(
10141044
// `TOUCH_FORCE_FIXED_SCALE` in `events.rs`.
10151045
private const val TOUCH_POSITION_SCALE: Float = 1024f
10161046
private const val TOUCH_FORCE_SCALE: Float = 10_000f
1047+
1048+
/**
1049+
* Live attached-host count across the JVM. When > 1, every host
1050+
* shares the process with at least one sibling that owns its own
1051+
* HGLRC and DirectContext (e.g., main window + DecoratedDialog).
1052+
* Skia's per-DirectContext GL state cache can drift any time the
1053+
* other host's onRedrawRequested swaps WGL contexts behind our
1054+
* back, so we resetGLAll on every frame entry in that regime.
1055+
* The flag-gated path stays for the single-host case to keep the
1056+
* single-window hot path cheap.
1057+
*/
1058+
private val attachedHostCount =
1059+
java
1060+
.util
1061+
.concurrent
1062+
.atomic
1063+
.AtomicInteger(0)
10171064
}
10181065

10191066
private inner class FlushingMainDispatcher : CoroutineDispatcher() {

decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/render/TaoPopupHostWindows.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ internal interface TaoPopupHostWindows {
2929
/** Host window's content size in physical pixels. */
3030
val parentWindowSize: IntSize
3131

32+
/**
33+
* Screen work area in physical pixels. Used as the inner scene's
34+
* layout size so a tall popup (DropdownMenu, expanded Tooltip) in a
35+
* small parent window lays out at full height instead of being
36+
* artificially clipped by the owner window's bounds. Mirrors the
37+
* macOS [TaoPopupHost.workAreaSize] contract. Defaults to
38+
* [parentWindowSize] when the host can't resolve the monitor.
39+
*/
40+
val workAreaSize: IntSize get() = parentWindowSize
41+
3242
/** Coroutine context to feed inner scenes. */
3343
val sceneCoroutineContext: CoroutineContext
3444

decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/render/TaoPopupSceneLayer.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,10 @@ internal class TaoPopupSceneLayer(
9292
* height + the `Popup.PositionProvider` flips/clips at the screen
9393
* edge. Read once; not reactive (the popup is rebuilt on owner-move).
9494
*/
95-
private val sceneLayoutSize: IntSize = host.workAreaSize.let {
96-
IntSize(it.width.coerceAtLeast(1), it.height.coerceAtLeast(1))
97-
}
95+
private val sceneLayoutSize: IntSize =
96+
host.workAreaSize.let {
97+
IntSize(it.width.coerceAtLeast(1), it.height.coerceAtLeast(1))
98+
}
9899

99100
/**
100101
* Panel created at parent-window-size offscreen so the inner scene

decorated-window-tao/src/main/kotlin/dev/nucleusframework/window/tao/render/TaoPopupSceneLayerWindows.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ internal class TaoPopupSceneLayerWindows(
6262
private var heightPx: Int = host.parentWindowSize.height.coerceAtLeast(1)
6363
private val scale: Float = host.scale
6464

65+
/**
66+
* Layout-size constraint for the inner [CanvasLayersComposeScene] —
67+
* mirrors [TaoPopupSceneLayer] on macOS. The popup HWND itself (and
68+
* the WGL framebuffer) stays at `widthPx`/`heightPx` so we don't
69+
* allocate a full-screen-sized surface, but the scene is laid out
70+
* with screen-work-area constraints so popups anchored near the
71+
* window edges (Tooltip in title bar, DropdownMenu in a small
72+
* floating window…) can flip/extend without being artificially
73+
* clipped — which manifested as an empty-content flicker on Windows
74+
* when the parent's `parentWindowSize` constraint conflicted with
75+
* the Popup.PositionProvider's layout pass.
76+
*/
77+
private val sceneLayoutSize: IntSize =
78+
host.workAreaSize.let {
79+
IntSize(it.width.coerceAtLeast(1), it.height.coerceAtLeast(1))
80+
}
81+
6582
private val panelHandle: Long =
6683
PopupNativeBridgeWindows
6784
.nativeCreatePanel(
@@ -87,7 +104,7 @@ internal class TaoPopupSceneLayerWindows(
87104
CanvasLayersComposeScene(
88105
density = _density,
89106
layoutDirection = _layoutDirection,
90-
size = IntSize(widthPx, heightPx),
107+
size = sceneLayoutSize,
91108
coroutineContext = host.sceneCoroutineContext,
92109
platformContext =
93110
object : PlatformContext.Empty() {

decorated-window-tao/src/main/native/windows/nucleus_tao_windows_deco.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,3 +541,4 @@ Java_dev_nucleusframework_window_tao_NativeTaoWindowsDecoBridge_nativeGetWindowR
541541
(*env)->SetLongArrayRegion(env, arr, 0, 4, values);
542542
return arr;
543543
}
544+

0 commit comments

Comments
 (0)