diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel b/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel
index a8ff0445dd99..0e8a5f2ca478 100644
--- a/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel
+++ b/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel
@@ -66,6 +66,10 @@ jvm_library(
"//libraries/kotlinx/coroutines/test:test_test_lib",
"//platform/jewel/ui",
"//platform/jewel/ui:ui_test_lib",
+ "//libraries/compose-foundation-desktop",
+ "//libraries/compose-foundation-desktop:compose-foundation-desktop_test_lib",
+ "//libraries/jna",
+ "//libraries/jna:jna_test_lib",
],
)
### auto-generated section `build intellij.platform.jewel.intUi.standalone.tests` end
diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts b/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts
index d570d0f35c45..73e4962d9fbd 100644
--- a/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts
+++ b/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts
@@ -1,4 +1,8 @@
-plugins { jewel }
+plugins {
+ jewel
+ alias(libs.plugins.composeDesktop)
+ alias(libs.plugins.compose.compiler)
+}
dependencies {
api(projects.intUi.intUiStandalone)
@@ -9,6 +13,8 @@ dependencies {
testRuntimeOnly(libs.junit.platform.launcher)
testImplementation(libs.kotlinx.coroutines.core)
testImplementation(libs.kotlinx.coroutines.test)
+ testImplementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") }
+ testImplementation(libs.jna.core)
}
tasks.test { useJUnitPlatform() }
diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml b/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml
index 36f48c07db6b..65f76bf7cf3a 100644
--- a/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml
+++ b/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml
@@ -39,5 +39,7 @@
+
+
\ No newline at end of file
diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesReflectionTest.kt b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesReflectionTest.kt
deleted file mode 100644
index 0b8c904ea6b4..000000000000
--- a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesReflectionTest.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
-package org.jetbrains.jewel.intui.standalone.window.macos
-
-import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
-import kotlin.test.assertNull
-import org.junit.jupiter.api.Test
-
-/**
- * Regression test for JEWEL-1387: the reflective lookups used to resolve the native window must search the whole class
- * hierarchy, not just the runtime class.
- *
- * On JBR 25 the AWT window peer is `sun.lwawt.macosx.LWCWindowPeer`, a subclass of `sun.lwawt.LWWindowPeer` that
- * inherits (but does not declare) `getPlatformWindow()`. A `getDeclaredMethod` call on the runtime class alone throws
- * [NoSuchMethodException] and silently breaks native window chrome updates on macOS.
- */
-internal class MacPlatformServicesReflectionTest {
- @Suppress("UnusedPrivateMember", "FunctionOnlyReturningConstant")
- private open class BasePeer {
- @JvmField internal val ptr: Long = 42L
-
- fun getPlatformWindow(): Any = this
- }
-
- // Mimics JBR 25's LWCWindowPeer: inherits getPlatformWindow() and ptr without declaring them
- private class SubclassPeer : BasePeer()
-
- @Test
- fun `findMethodInHierarchy finds method declared on the class itself`() {
- val method = MacPlatformServicesDefaultImpl.findMethodInHierarchy(BasePeer::class.java, "getPlatformWindow")
-
- assertNotNull(method, "Should find a method declared directly on the class")
- }
-
- @Test
- fun `findMethodInHierarchy finds method inherited from a superclass`() {
- val method = MacPlatformServicesDefaultImpl.findMethodInHierarchy(SubclassPeer::class.java, "getPlatformWindow")
-
- assertNotNull(method, "Should find a method declared on a superclass (JBR 25 LWCWindowPeer scenario)")
- assertEquals(BasePeer::class.java, method.declaringClass, "Method should be resolved from the superclass")
- }
-
- @Test
- fun `findMethodInHierarchy returns null for a missing method`() {
- val method = MacPlatformServicesDefaultImpl.findMethodInHierarchy(SubclassPeer::class.java, "doesNotExist")
-
- assertNull(method, "Should return null instead of throwing for missing methods")
- }
-
- @Test
- fun `findFieldInHierarchy finds field declared on the class itself`() {
- val field = MacPlatformServicesDefaultImpl.findFieldInHierarchy(BasePeer::class.java, "ptr")
-
- assertNotNull(field, "Should find a field declared directly on the class")
- }
-
- @Test
- fun `findFieldInHierarchy finds field inherited from a superclass`() {
- val field = MacPlatformServicesDefaultImpl.findFieldInHierarchy(SubclassPeer::class.java, "ptr")
-
- assertNotNull(field, "Should find a field declared on a superclass")
- assertEquals(BasePeer::class.java, field.declaringClass, "Field should be resolved from the superclass")
- }
-
- @Test
- fun `findFieldInHierarchy returns null for a missing field`() {
- val field = MacPlatformServicesDefaultImpl.findFieldInHierarchy(SubclassPeer::class.java, "doesNotExist")
-
- assertNull(field, "Should return null instead of throwing for missing fields")
- }
-}
diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesWindowHandleTest.kt b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesWindowHandleTest.kt
new file mode 100644
index 000000000000..e70eb5dd1801
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServicesWindowHandleTest.kt
@@ -0,0 +1,55 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.intui.standalone.window.macos
+
+import androidx.compose.ui.awt.ComposeWindow
+import javax.swing.JFrame
+import javax.swing.SwingUtilities
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.condition.DisabledIf
+
+/**
+ * Regression test for JEWEL-1388: resolving the native `NSWindow*` must not require `sun.misc.Unsafe` or reflection
+ * into JDK-internal `sun.awt`/`sun.lwawt.macosx` classes.
+ */
+internal class MacPlatformServicesWindowHandleTest {
+ @Test
+ fun `getWindowFromJavaWindow returns NIL for a null window`() {
+ assertEquals(ID.NIL, MacPlatformServicesDefaultImpl.getWindowFromJavaWindow(null))
+ }
+
+ @Test
+ @DisabledIf(HEADLESS)
+ fun `getWindowFromJavaWindow returns NIL for a non-Compose AWT window`() {
+ val frame = JFrame()
+ try {
+ assertEquals(ID.NIL, MacPlatformServicesDefaultImpl.getWindowFromJavaWindow(frame))
+ } finally {
+ frame.dispose()
+ }
+ }
+
+ @Test
+ @DisabledIf(HEADLESS)
+ fun `getWindowFromJavaWindow returns the native handle of a realized ComposeWindow`() {
+ lateinit var window: ComposeWindow
+ try {
+ var handle = 0L
+ SwingUtilities.invokeAndWait {
+ window = ComposeWindow()
+ window.isVisible = true
+ handle = window.windowHandle
+ }
+
+ assertNotEquals(0L, handle, "A displayed ComposeWindow must expose a native window handle")
+ assertEquals(ID(handle), MacPlatformServicesDefaultImpl.getWindowFromJavaWindow(window))
+ } finally {
+ SwingUtilities.invokeAndWait { window.dispose() }
+ }
+ }
+
+ private companion object {
+ const val HEADLESS = "java.awt.GraphicsEnvironment#isHeadless"
+ }
+}
diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/UnsafeAccessing.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/UnsafeAccessing.kt
deleted file mode 100644
index c5cd754ae306..000000000000
--- a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/UnsafeAccessing.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
-package org.jetbrains.jewel.intui.standalone.window
-
-import java.lang.reflect.AccessibleObject
-import java.util.logging.Level
-import java.util.logging.Logger
-import sun.misc.Unsafe
-
-internal object UnsafeAccessing {
- private val logger = Logger.getLogger(UnsafeAccessing::class.java.simpleName)
-
- private val unsafe: Any? by lazy {
- try {
- val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe")
- theUnsafe.isAccessible = true
- theUnsafe.get(null) as Unsafe
- } catch (@Suppress("TooGenericExceptionCaught") error: Throwable) {
- logger.log(Level.WARNING, "Unsafe accessing initializing failed.", error)
- null
- }
- }
-
- val desktopModule by lazy { ModuleLayer.boot().findModule("java.desktop").get() }
-
- val ownerModule: Module by lazy { this.javaClass.module }
-
- private val isAccessibleFieldOffset: Long? by lazy {
- try {
- @Suppress("DEPRECATION")
- (unsafe as? Unsafe)?.objectFieldOffset(Parent::class.java.getDeclaredField("first"))
- } catch (_: Throwable) {
- null
- }
- }
-
- private val implAddOpens by lazy {
- try {
- Module::class.java.getDeclaredMethod("implAddOpens", String::class.java, Module::class.java).accessible()
- } catch (_: Throwable) {
- null
- }
- }
-
- fun assignAccessibility(obj: AccessibleObject) {
- try {
- val theUnsafe = unsafe as? Unsafe ?: return
- val offset = isAccessibleFieldOffset ?: return
- theUnsafe.putBooleanVolatile(obj, offset, true)
- } catch (_: Throwable) {
- // ignore
- }
- }
-
- fun assignAccessibility(module: Module, packages: List) {
- try {
- packages.forEach { implAddOpens?.invoke(module, it, ownerModule) }
- } catch (_: Throwable) {
- // ignore
- }
- }
-
- private class Parent {
- var first = false
-
- @Volatile var second: Any? = null
- }
-}
-
-internal fun T.accessible(): T = apply { UnsafeAccessing.assignAccessibility(this) }
diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt
index 86fca4ad7b59..f98427a71a42 100644
--- a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt
+++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/window/macos/MacPlatformServices.kt
@@ -3,13 +3,10 @@ package org.jetbrains.jewel.intui.standalone.window.macos
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.awt.ComposeWindow
import com.sun.jna.Callback
import com.sun.jna.Pointer
-import java.awt.Component
import java.awt.Window
-import java.lang.reflect.Field
-import java.lang.reflect.InvocationTargetException
-import java.lang.reflect.Method
import javax.swing.SwingUtilities
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.VisibleForTesting
@@ -17,132 +14,63 @@ import org.jetbrains.jewel.foundation.InternalJewelApi
import org.jetbrains.jewel.foundation.util.JewelLogger
import org.jetbrains.jewel.intui.standalone.styling.default
import org.jetbrains.jewel.intui.standalone.styling.macOs
-import org.jetbrains.jewel.intui.standalone.window.UnsafeAccessing
-import org.jetbrains.jewel.intui.standalone.window.accessible
import org.jetbrains.jewel.ui.component.styling.ScrollbarVisibility
import org.jetbrains.jewel.ui.component.styling.TrackClickBehavior
import org.jetbrains.skiko.hostOs
+/**
+ * Provides macOS-specific platform services for window decoration and system preference observation.
+ *
+ * Implementations communicate with macOS APIs (via JNA/Objective-C) to apply native window chrome updates and read
+ * scrollbar settings from native sources such as NSUserDefaults (track-click behavior) and NSScroller (scroller style)
+ */
@ApiStatus.Internal
@InternalJewelApi
public interface MacPlatformServices {
+ /** Updates the native window's color scheme to match the current Jewel theme. */
public fun updateColors(w: Window)
+ /** Refreshes the full-screen button state in the native window title bar. */
public fun updateFullScreenButtons(w: Window)
+ /** Hides the system cursor until the next mouse-move event (delegates to `NSCursor`). */
public fun hideCursorUntilMoved()
+ /** Reads the current scrollbar track-click behavior from `NSUserDefaults`. */
public fun readScrollbarTrackClickBehavior(): TrackClickBehavior
+ /** Reads the current scrollbar visibility style from the native `NSScroller` preference. */
public fun readScrollbarVisibility(): ScrollbarVisibility
+ /**
+ * Registers [action] to be invoked whenever a scrollbar-related system preference changes.
+ *
+ * Observes both `NSPreferredScrollerStyleDidChangeNotification` (visibility) and
+ * `AppleNoRedisplayAppearancePreferenceChanged` (track-click behavior).
+ */
public fun onPreferencesChanged(action: () -> Unit)
}
+/** Default [MacPlatformServices] implementation that uses JNA to invoke native macOS (Objective-C) APIs. */
@ApiStatus.Internal
@InternalJewelApi
public object MacPlatformServicesDefaultImpl : MacPlatformServices {
private val logger = JewelLogger.getInstance(MacPlatformServicesDefaultImpl::class.java.simpleName)
private var nativeCallbackReference: Callback? = null // Keep a strong reference here to prevent GC
- init {
- try {
- UnsafeAccessing.assignAccessibility(
- UnsafeAccessing.desktopModule,
- listOf("sun.awt", "sun.lwawt", "sun.lwawt.macosx"),
- )
- } catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
- logger.warn("Assign access for jdk.desktop failed.", e)
- }
- }
-
- internal fun getWindowFromJavaWindow(w: Window?): ID {
- if (w == null) {
- return ID.NIL
- }
- try {
- val cPlatformWindow = getPlatformWindow(w)
- if (cPlatformWindow != null) {
- val ptr = findFieldInHierarchy(cPlatformWindow.javaClass, "ptr")
- if (ptr == null) {
- logger.warn("Fail to get cPlatformWindow from awt window: no 'ptr' field found.")
- return ID.NIL
- }
- ptr.setAccessible(true)
- return ID(ptr.getLong(cPlatformWindow))
- }
- } catch (e: IllegalAccessException) {
- logger.warn("Fail to get cPlatformWindow from awt window.", e)
- }
- return ID.NIL
- }
-
- public fun getPlatformWindow(w: Window): Any? {
- try {
- val awtAccessor = Class.forName("sun.awt.AWTAccessor")
- val componentAccessor = awtAccessor.getMethod("getComponentAccessor").invoke(null)
- val getPeer = componentAccessor.javaClass.getMethod("getPeer", Component::class.java).accessible()
- val peer = getPeer.invoke(componentAccessor, w)
- if (peer != null) {
- // The method may be declared on a superclass of the runtime peer class (e.g., on JBR 25 the peer
- // is sun.lwawt.macosx.LWCWindowPeer, which inherits getPlatformWindow() from sun.lwawt.LWWindowPeer),
- // so we must search the whole class hierarchy rather than just the runtime class.
- val getPlatformWindowMethod = findMethodInHierarchy(peer.javaClass, "getPlatformWindow")
- if (getPlatformWindowMethod == null) {
- logger.warn("Fail to get cPlatformWindow from awt window: no getPlatformWindow() method found.")
- return null
- }
- val cPlatformWindow = getPlatformWindowMethod.invoke(peer)
- if (cPlatformWindow != null) {
- return cPlatformWindow
- }
- }
- } catch (e: IllegalAccessException) {
- logger.warn("Fail to get cPlatformWindow from awt window.", e)
- } catch (e: InvocationTargetException) {
- logger.warn("Fail to get cPlatformWindow from awt window.", e)
- } catch (e: ClassNotFoundException) {
- logger.warn("Fail to get cPlatformWindow from awt window.", e)
- }
- return null
- }
-
/**
- * Finds a method with the given [name] and no parameters declared on [clazz] or any of its superclasses, or `null`
- * if no such method exists.
+ * Returns the native `NSWindow*` backing the given AWT [Window], or [ID.NIL] if [w] isn't a [ComposeWindow] or the
+ * native surface hasn't been realized yet.
+ *
+ * Uses [ComposeWindow.windowHandle], Compose's own supported accessor, instead of reflecting into JDK-internal
+ * `sun.awt`/`sun.lwawt.macosx` classes (see JEWEL-1388).
*/
@VisibleForTesting
@ApiStatus.Internal
@InternalJewelApi
- public fun findMethodInHierarchy(clazz: Class<*>, name: String): Method? {
- var current: Class<*>? = clazz
- while (current != null) {
- try {
- return current.getDeclaredMethod(name)
- } catch (_: NoSuchMethodException) {
- current = current.superclass
- }
- }
- return null
- }
-
- /**
- * Finds a field with the given [name] declared on [clazz] or any of its superclasses, or `null` if no such field
- * exists.
- */
- @VisibleForTesting
- @ApiStatus.Internal
- @InternalJewelApi
- public fun findFieldInHierarchy(clazz: Class<*>, name: String): Field? {
- var current: Class<*>? = clazz
- while (current != null) {
- try {
- return current.getDeclaredField(name)
- } catch (_: NoSuchFieldException) {
- current = current.superclass
- }
- }
- return null
+ public fun getWindowFromJavaWindow(w: Window?): ID {
+ val handle = (w as? ComposeWindow)?.windowHandle ?: 0L
+ return if (handle == 0L) ID.NIL else ID(handle)
}
public override fun updateColors(w: Window) {
@@ -282,6 +210,10 @@ public object MacPlatformServicesDefaultImpl : MacPlatformServices {
}
}
+ /**
+ * Executes [producer] inside a `NSAutoreleasePool` and returns its result, or `null` if the current OS is not macOS
+ * or if an exception is thrown (the exception is logged as a warning).
+ */
@Suppress("TooGenericExceptionCaught")
public fun callMac(producer: () -> T?): T? {
if (!hostOs.isMacOS) return null
@@ -298,6 +230,7 @@ public object MacPlatformServicesDefaultImpl : MacPlatformServices {
}
}
+/** [ProvidableCompositionLocal] that provides the active [MacPlatformServices] instance for the current window. */
@get:ApiStatus.Internal
@InternalJewelApi
public val LocalMacPlatformServices: ProvidableCompositionLocal = staticCompositionLocalOf {