Skip to content

Commit b43a742

Browse files
authored
Merge pull request #121 from compose-fluent/gallery/update_windows_title_bar
[gallery] Update windows title bar
2 parents 235e0eb + a411699 commit b43a742

File tree

3 files changed

+66
-80
lines changed

3 files changed

+66
-80
lines changed

gallery/proguard-rules.desktop.pro

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
11
-dontwarn com.sun.jna.internal.Cleaner
22
-keep class com.sun.jna.** { *; }
3-
-keep class * implements com.sun.jna.** { *; }
3+
-keep class * implements com.sun.jna.** { *; }
4+
5+
# LayoutHitTestOwner ProGuard rules
6+
-keep class androidx.compose.foundation.HoverableNode { *; }
7+
-keep class androidx.compose.foundation.gestures.ScrollableNode { *; }
8+
9+
-keep class androidx.compose.ui.scene.PlatformLayersComposeSceneImpl { *; }
10+
-keep class androidx.compose.ui.scene.CanvasLayersComposeSceneImpl { *; }
11+
-keep class androidx.compose.ui.scene.CanvasLayersComposeSceneImpl$AttachedComposeSceneLayer { *; }
12+
13+
-keepclassmembers class androidx.compose.ui.scene.PlatformLayersComposeSceneImpl {
14+
private *** getMainOwner();
15+
}
16+
17+
-keepclassmembers class androidx.compose.ui.scene.CanvasLayersComposeSceneImpl {
18+
private *** mainOwner;
19+
private *** _layersCopyCache;
20+
private *** focusedLayer;
21+
}
22+
23+
-keepclassmembers class androidx.compose.ui.scene.CanvasLayersComposeSceneImpl$AttachedComposeSceneLayer {
24+
private *** owner;
25+
private *** isInBounds(...);
26+
}

gallery/src/desktopMain/kotlin/io/github/composefluent/gallery/jna/windows/ComposeWindowProcedure.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import com.sun.jna.platform.win32.WinDef.LPARAM
4444
import com.sun.jna.platform.win32.WinDef.WPARAM
4545
import com.sun.jna.platform.win32.WinDef.LRESULT
4646
import com.sun.jna.platform.win32.BaseTSD.LONG_PTR
47+
import com.sun.jna.platform.win32.WinDef
4748
import com.sun.jna.platform.win32.WinDef.HMENU
4849
import com.sun.jna.platform.win32.WinNT
4950
import com.sun.jna.platform.win32.WinReg
@@ -103,6 +104,7 @@ internal class ComposeWindowProcedure(
103104
SkiaLayerWindowProcedure(
104105
skiaLayer = it,
105106
hitTest = { x, y ->
107+
updateWindowInfo()
106108
val horizontalPadding = frameX
107109
val verticalPadding = frameY
108110
// Hit test for resizer border
@@ -267,6 +269,24 @@ internal class ComposeWindowProcedure(
267269
}
268270
}
269271

272+
// Force update window info that resolve the hit test result is incorrect when user moving window to another monitor.
273+
private fun updateWindowInfo() {
274+
User32Extend.instance?.apply {
275+
dpi = GetDpiForWindow(windowHandle)
276+
frameX = GetSystemMetricsForDpi(WinUser.SM_CXFRAME, dpi)
277+
frameY = GetSystemMetricsForDpi(WinUser.SM_CYFRAME, dpi)
278+
279+
val rect = WinDef.RECT()
280+
if (GetWindowRect(windowHandle, rect)) {
281+
rect.read()
282+
width = rect.right - rect.left
283+
height = rect.bottom - rect.top
284+
}
285+
rect.clear()
286+
}
287+
288+
}
289+
270290
private fun updateMenuItemInfo(menu: HMENU, menuItemInfo: MENUITEMINFO, item: Int, enabled: Boolean) {
271291
menuItemInfo.fState = if (enabled) WinUserConst.MFS_ENABLED else WinUserConst.MFS_DISABLED
272292
User32Extend.instance?.SetMenuItemInfo(menu, item, false, menuItemInfo)

gallery/src/desktopMain/kotlin/io/github/composefluent/gallery/window/LayoutHitTestOwner.kt

Lines changed: 22 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
1-
@file:Suppress("UNCHECKED_CAST")
1+
@file:Suppress("UNCHECKED_CAST", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "DEPRECATED")
22

33
package io.github.jpy.wangposefluent.gallery.window
44

55
import androidx.compose.runtime.Composable
6-
import androidx.compose.runtime.ProvidableCompositionLocal
7-
import androidx.compose.runtime.Stable
86
import androidx.compose.runtime.remember
97
import androidx.compose.ui.InternalComposeUiApi
10-
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.geometry.Offset
9+
import androidx.compose.ui.node.HitTestResult
10+
import androidx.compose.ui.node.LayoutNode
1111
import androidx.compose.ui.node.PointerInputModifierNode
12+
import androidx.compose.ui.node.RootNodeOwner
1213
import androidx.compose.ui.scene.ComposeScene
14+
import androidx.compose.ui.scene.CopiedList
15+
import androidx.compose.ui.scene.LocalComposeScene
1316
import androidx.compose.ui.util.fastForEachReversed
1417
import androidx.compose.ui.util.packFloats
1518

1619
@OptIn(InternalComposeUiApi::class)
1720
@Composable
1821
fun rememberLayoutHitTestOwner(): LayoutHitTestOwner {
19-
val scene = getLocalComposeScene()?.current ?: error("no compose scene")
22+
// TODO Remove LocalComposeScene
23+
val scene = LocalComposeScene.current ?: error("no compose scene")
2024
return remember(scene) {
2125
when(scene::class.qualifiedName) {
2226
"androidx.compose.ui.scene.CanvasLayersComposeSceneImpl" -> {
@@ -30,16 +34,6 @@ fun rememberLayoutHitTestOwner(): LayoutHitTestOwner {
3034
}
3135
}
3236

33-
@OptIn(InternalComposeUiApi::class)
34-
@Stable
35-
private fun getLocalComposeScene(): ProvidableCompositionLocal<ComposeScene>? {
36-
val classLoader = ComposeScene::class.java.classLoader
37-
val composeSceneClass = classLoader.loadClass("androidx.compose.ui.scene.ComposeScene_skikoKt")
38-
val methodRef = composeSceneClass.getMethod("getLocalComposeScene")
39-
methodRef.trySetAccessible()
40-
return methodRef.invoke(null) as? ProvidableCompositionLocal<ComposeScene>
41-
}
42-
4337
interface LayoutHitTestOwner {
4438

4539
fun hitTest(x: Float, y: Float): Boolean {
@@ -48,73 +42,28 @@ interface LayoutHitTestOwner {
4842
}
4943

5044
/*
51-
* reflect implementation for compose 1.6
45+
* reflect implementation for compose 1.8
5246
*/
5347
internal abstract class ReflectLayoutHitTestOwner: LayoutHitTestOwner {
5448

5549
@OptIn(InternalComposeUiApi::class)
5650
protected val classLoader = ComposeScene::class.java.classLoader!!
5751

58-
private val rootNodeOwnerOwnerField = classLoader.loadClass("androidx.compose.ui.node.RootNodeOwner")
59-
.getDeclaredField("owner").apply {
60-
trySetAccessible()
61-
}
62-
63-
private val ownerRootField = classLoader.loadClass("androidx.compose.ui.node.Owner")
64-
.getDeclaredMethod("getRoot").apply {
65-
trySetAccessible()
66-
}
67-
68-
private val hitTestResultClass = classLoader.loadClass("androidx.compose.ui.node.HitTestResult")
69-
70-
private val layoutNodeHitTestMethod = classLoader.loadClass("androidx.compose.ui.node.LayoutNode")
71-
.declaredMethods.first { it.name.startsWith("hitTest-") && it.parameterCount == 4 }
72-
73-
protected fun getLayoutNode(rootNodeOwner: Any): Any {
74-
val owner = rootNodeOwnerOwnerField.get(rootNodeOwner)
75-
return ownerRootField.invoke(owner)
52+
protected fun getLayoutNode(rootNodeOwner: RootNodeOwner): LayoutNode {
53+
return rootNodeOwner.owner.root
7654
}
7755

78-
protected fun Any.layoutNodeHitTest(x: Float, y: Float): Boolean {
56+
protected fun LayoutNode.layoutNodeHitTest(x: Float, y: Float): Boolean {
7957
try {
80-
val result = hitTestResultClass.getDeclaredConstructor().newInstance()
81-
// Try with the original parameter order
82-
try {
83-
layoutNodeHitTestMethod.invoke(this, packFloats(x, y), result, false, true)
84-
} catch (e: IllegalArgumentException) {
85-
// If that fails, try with a different parameter order
86-
try {
87-
layoutNodeHitTestMethod.invoke(this, packFloats(x, y), result, true, false)
88-
} catch (e2: IllegalArgumentException) {
89-
// If both fail, try without the boolean parameters
90-
layoutNodeHitTestMethod.invoke(this, packFloats(x, y), result)
91-
}
92-
}
93-
val resultAsList = result as? List<*> ?: return false
94-
val lastNode = resultAsList.lastOrNull()
58+
val result = HitTestResult()
59+
this.hitTest(pointerPosition = Offset(x, y), hitTestResult = result)
60+
val lastNode = result.lastOrNull()
9561
return lastNode is PointerInputModifierNode
9662
} catch (e: Exception) {
9763
// If anything goes wrong, return false to be safe
9864
return false
9965
}
10066
}
101-
102-
protected class CopiedList<T>(
103-
private val populate: (MutableList<T>) -> Unit
104-
) : MutableList<T> by mutableListOf() {
105-
inline fun withCopy(
106-
block: (List<T>) -> Unit
107-
) {
108-
// In case of recursive calls, allocate new list
109-
val copy = if (isEmpty()) this else mutableListOf()
110-
populate(copy)
111-
try {
112-
block(copy)
113-
} finally {
114-
copy.clear()
115-
}
116-
}
117-
}
11867
}
11968

12069
@OptIn(InternalComposeUiApi::class)
@@ -123,11 +72,11 @@ internal class PlatformLayersLayoutHitTestOwner(scene: ComposeScene) : ReflectLa
12372

12473
private val mainOwnerRef = sceneClass.getDeclaredMethod("getMainOwner").let {
12574
it.trySetAccessible()
126-
it.invoke(scene)
75+
it.invoke(scene) as RootNodeOwner
12776
}
12877

12978
override fun hitTest(x: Float, y: Float): Boolean {
130-
return getLayoutNode(mainOwnerRef).layoutNodeHitTest(x, y)
79+
return mainOwnerRef.owner.root.layoutNodeHitTest(x, y)
13180
}
13281
}
13382

@@ -138,13 +87,13 @@ internal class CanvasLayersLayoutHitTestOwner(private val scene: ComposeScene) :
13887

13988
private val mainOwnerRef = sceneClass.getDeclaredField("mainOwner").let {
14089
it.trySetAccessible()
141-
it.get(scene)
90+
it.get(scene) as RootNodeOwner
14291
}
14392

144-
private val layersRef = sceneClass.getDeclaredField("layers").let {
93+
private val _layers = sceneClass.getDeclaredField("_layersCopyCache").let {
14594
it.trySetAccessible()
14695
it.get(scene)
147-
} as MutableList<Any>
96+
} as CopiedList<*>
14897

14998
private val focusedLayerField = sceneClass.getDeclaredField("focusedLayer").apply {
15099
trySetAccessible()
@@ -160,17 +109,11 @@ internal class CanvasLayersLayoutHitTestOwner(private val scene: ComposeScene) :
160109
trySetAccessible()
161110
}
162111

163-
private val _layers = CopiedList {
164-
for (layer in layersRef) {
165-
it.add(layer)
166-
}
167-
}
168-
169112
override fun hitTest(x: Float, y: Float): Boolean {
170113
_layers.withCopy {
171114
it.fastForEachReversed { layer ->
172115
if (layerIsInBoundMethod.invoke(layer, packFloats(x, y)) == true) {
173-
return getLayoutNode(layerOwnerField.get(layer)).layoutNodeHitTest(x, y)
116+
return getLayoutNode(layerOwnerField.get(layer) as RootNodeOwner).layoutNodeHitTest(x, y)
174117
} else if (layer == focusedLayerField.get(scene)) {
175118
return false
176119
}

0 commit comments

Comments
 (0)