diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePlatformCursorController.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePlatformCursorController.kt
new file mode 100644
index 0000000000000..878c00f758eec
--- /dev/null
+++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePlatformCursorController.kt
@@ -0,0 +1,15 @@
+// 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.bridge
+
+import com.intellij.util.ui.MacUIUtil
+import org.jetbrains.jewel.ui.platform.PlatformCursorController
+import org.jetbrains.skiko.OS
+import org.jetbrains.skiko.hostOs
+
+internal object BridgePlatformCursorController : PlatformCursorController {
+ override fun hideCursorWhileTyping() {
+ if (hostOs == OS.MacOS) {
+ MacUIUtil.hideCursor()
+ }
+ }
+}
diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/SwingBridgeTheme.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/SwingBridgeTheme.kt
index c80cc6d7bf0f6..6c46990c3ca22 100644
--- a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/SwingBridgeTheme.kt
+++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/SwingBridgeTheme.kt
@@ -11,6 +11,7 @@ import androidx.compose.ui.platform.LocalUriHandler
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.jewel.bridge.BridgeMessageResourceResolver
import org.jetbrains.jewel.bridge.BridgePainterHintsProvider
+import org.jetbrains.jewel.bridge.BridgePlatformCursorController
import org.jetbrains.jewel.bridge.BridgeTypography
import org.jetbrains.jewel.bridge.BridgeUriHandler
import org.jetbrains.jewel.bridge.SwingBridgeReader
@@ -26,6 +27,7 @@ import org.jetbrains.jewel.ui.LocalMenuItemShortcutProvider
import org.jetbrains.jewel.ui.LocalTypography
import org.jetbrains.jewel.ui.icon.LocalNewUiChecker
import org.jetbrains.jewel.ui.painter.LocalPainterHintsProvider
+import org.jetbrains.jewel.ui.platform.LocalPlatformCursorController
import org.jetbrains.jewel.ui.theme.BaseJewelTheme
import org.jetbrains.jewel.ui.util.LocalMessageResourceResolverProvider
@@ -52,6 +54,7 @@ public fun SwingBridgeTheme(content: @Composable () -> Unit) {
LocalTypography provides BridgeTypography,
LocalUriHandler provides BridgeUriHandler,
LocalMessageResourceResolverProvider provides BridgeMessageResourceResolver(),
+ LocalPlatformCursorController provides BridgePlatformCursorController,
) {
content()
}
diff --git a/platform/jewel/int-ui/int-ui-standalone/BUILD.bazel b/platform/jewel/int-ui/int-ui-standalone/BUILD.bazel
index 472762446226b..8508164f6da9f 100644
--- a/platform/jewel/int-ui/int-ui-standalone/BUILD.bazel
+++ b/platform/jewel/int-ui/int-ui-standalone/BUILD.bazel
@@ -37,6 +37,7 @@ jvm_library(
"//libraries/compose-runtime-desktop",
"//platform/jewel/foundation",
"//libraries/jbr",
+ "@lib//:jna",
],
plugins = ["@lib//:compose-plugin"]
)
diff --git a/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts b/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts
index 5f66125caaadb..7defc34d3b345 100644
--- a/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts
+++ b/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts
@@ -14,6 +14,7 @@ plugins {
dependencies {
api(projects.ui)
implementation(libs.jbr.api)
+ implementation(libs.jna.core)
}
intelliJThemeGenerator {
diff --git a/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml b/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml
index 10982c8de6b22..43b13372bf523 100644
--- a/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml
+++ b/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml
@@ -41,5 +41,6 @@
+
\ No newline at end of file
diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/StandalonePlatformCursorController.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/StandalonePlatformCursorController.kt
new file mode 100644
index 0000000000000..5c2faba37d7bb
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/StandalonePlatformCursorController.kt
@@ -0,0 +1,56 @@
+// Copyright 2000-2025 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
+
+import com.sun.jna.Library
+import com.sun.jna.Native
+import com.sun.jna.Pointer
+import org.jetbrains.jewel.ui.platform.PlatformCursorController
+import org.jetbrains.skiko.OS
+import org.jetbrains.skiko.hostOs
+
+/**
+ * Standalone implementation of [PlatformCursorController] for macOS.
+ *
+ * This implementation uses JNA to call the native Objective-C method `[NSCursor setHiddenUntilMouseMoves:]`. The
+ * implementation is based on the platform code in `com.intellij.util.ui.MacUIUtil.hideCursor()`, but uses JNA's
+ * `objc_msgSend` directly instead of the IJ Foundation framework, since IJ APIs are not available in standalone mode.
+ * the Foundation framework, since Foundation is not available in standalone mode.
+ *
+ * @see com.intellij.util.ui.MacUIUtil.hideCursor
+ */
+internal object StandalonePlatformCursorController : PlatformCursorController {
+ private val objcLibrary: ObjCLibrary? by lazy {
+ try {
+ Native.load("objc", ObjCLibrary::class.java)
+ } catch (_: Exception) {
+ null
+ }
+ }
+
+ override fun hideCursorWhileTyping() {
+ if (hostOs == OS.MacOS) {
+ try {
+ val lib = objcLibrary ?: return
+
+ val nsCursorClass = lib.objc_getClass("NSCursor")
+ if (nsCursorClass == null || Pointer.nativeValue(nsCursorClass) == 0L) return
+
+ val selector = lib.sel_registerName("setHiddenUntilMouseMoves:")
+ if (selector == null || Pointer.nativeValue(selector) == 0L) return
+
+ lib.objc_msgSend(nsCursorClass, selector, true)
+ } catch (_: Throwable) {
+ // Silently fail if native calls fail or if JNA is not available
+ }
+ }
+ }
+
+ @Suppress("ktlint:standard:function-naming")
+ private interface ObjCLibrary : Library {
+ fun objc_getClass(className: String?): Pointer?
+
+ fun sel_registerName(selectorName: String?): Pointer?
+
+ fun objc_msgSend(receiver: Pointer?, selector: Pointer?, vararg args: Any?)
+ }
+}
diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt
index 22e0ea17b1711..8128add14bbf5 100644
--- a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt
+++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt
@@ -19,6 +19,7 @@ import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme
import org.jetbrains.jewel.intui.standalone.IntUiMessageResourceResolver
import org.jetbrains.jewel.intui.standalone.IntUiTypography
import org.jetbrains.jewel.intui.standalone.StandalonePainterHintsProvider
+import org.jetbrains.jewel.intui.standalone.StandalonePlatformCursorController
import org.jetbrains.jewel.intui.standalone.icon.StandaloneNewUiChecker
import org.jetbrains.jewel.intui.standalone.menuShortcut.StandaloneMenuItemShortcutHintProvider
import org.jetbrains.jewel.intui.standalone.menuShortcut.StandaloneShortcutProvider
@@ -68,6 +69,7 @@ import org.jetbrains.jewel.ui.component.styling.TooltipAutoHideBehavior
import org.jetbrains.jewel.ui.component.styling.TooltipStyle
import org.jetbrains.jewel.ui.icon.LocalNewUiChecker
import org.jetbrains.jewel.ui.painter.LocalPainterHintsProvider
+import org.jetbrains.jewel.ui.platform.LocalPlatformCursorController
import org.jetbrains.jewel.ui.theme.BaseJewelTheme
import org.jetbrains.jewel.ui.util.LocalMessageResourceResolverProvider
@@ -727,6 +729,7 @@ public fun IntUiTheme(
LocalMenuItemShortcutHintProvider provides StandaloneMenuItemShortcutHintProvider,
LocalTypography provides IntUiTypography,
LocalMessageResourceResolverProvider provides IntUiMessageResourceResolver,
+ LocalPlatformCursorController provides StandalonePlatformCursorController,
) {
content()
}
diff --git a/platform/jewel/ui/api-dump.txt b/platform/jewel/ui/api-dump.txt
index 30483656ee0d1..0288cdb3b2456 100644
--- a/platform/jewel/ui/api-dump.txt
+++ b/platform/jewel/ui/api-dump.txt
@@ -3940,6 +3940,10 @@ f:org.jetbrains.jewel.ui.painter.hints.SizeKt
- sf:Size(I,I):org.jetbrains.jewel.ui.painter.PainterHint
f:org.jetbrains.jewel.ui.painter.hints.StatefulKt
- sf:Stateful(org.jetbrains.jewel.foundation.state.InteractiveComponentState):org.jetbrains.jewel.ui.painter.PainterHint
+org.jetbrains.jewel.ui.platform.PlatformCursorController
+- a:hideCursorWhileTyping():V
+f:org.jetbrains.jewel.ui.platform.PlatformCursorControllerKt
+- sf:getLocalPlatformCursorController():androidx.compose.runtime.ProvidableCompositionLocal
f:org.jetbrains.jewel.ui.theme.JewelThemeKt
- sf:BaseJewelTheme(org.jetbrains.jewel.foundation.theme.ThemeDefinition,org.jetbrains.jewel.ui.ComponentStyling,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I):V
- sf:BaseJewelTheme(org.jetbrains.jewel.foundation.theme.ThemeDefinition,org.jetbrains.jewel.ui.ComponentStyling,Z,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/InputField.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/InputField.kt
index 4823b846da538..eb466f02b7bf6 100644
--- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/InputField.kt
+++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/InputField.kt
@@ -27,6 +27,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.input.key.utf16CodePoint
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
@@ -45,6 +51,8 @@ import org.jetbrains.jewel.ui.Outline
import org.jetbrains.jewel.ui.component.styling.InputFieldStyle
import org.jetbrains.jewel.ui.focusOutline
import org.jetbrains.jewel.ui.outline
+import org.jetbrains.jewel.ui.platform.LocalPlatformCursorController
+import org.jetbrains.jewel.ui.platform.PlatformCursorController
@Composable
internal fun InputField(
@@ -120,6 +128,7 @@ internal fun InputField(
modifier =
modifier
.hoverable(hoverInteractionSource)
+ .thenIf(enabled && !readOnly) { hideCursorOnTyping() }
.then(backgroundModifier)
.thenIf(!undecorated && hasNoOutline) {
focusOutline(state = inputFieldState, outlineShape = shape, alignment = Stroke.Alignment.Center)
@@ -216,6 +225,7 @@ internal fun InputField(
modifier =
modifier
.hoverable(hoverInteractionSource)
+ .thenIf(enabled && !readOnly) { hideCursorOnTyping() }
.then(backgroundModifier)
.thenIf(!undecorated && hasNoOutline) {
focusOutline(state = inputFieldState, outlineShape = shape, alignment = Stroke.Alignment.Center)
@@ -239,6 +249,16 @@ internal fun InputField(
)
}
+@Composable
+private fun Modifier.hideCursorOnTyping(
+ cursorController: PlatformCursorController = LocalPlatformCursorController.current
+): Modifier = onPreviewKeyEvent { event ->
+ if (event.type == KeyEventType.KeyDown && event.key != Key.Escape && event.utf16CodePoint != 0) {
+ cursorController.hideCursorWhileTyping()
+ }
+ false // returning false so the text field can handle the key event properly
+}
+
@Immutable
@JvmInline
public value class InputFieldState(public val state: ULong) : FocusableComponentState {
diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/platform/PlatformCursorController.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/platform/PlatformCursorController.kt
new file mode 100644
index 0000000000000..039d655728fcb
--- /dev/null
+++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/platform/PlatformCursorController.kt
@@ -0,0 +1,21 @@
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.ui.platform
+
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/**
+ * Controller for platform-specific cursor behavior.
+ *
+ * This interface provides methods to control cursor visibility and behavior in a platform-specific way. Implementations
+ * should handle platform differences internally.
+ */
+public interface PlatformCursorController {
+ /** Hides the mouse cursor while the user is typing. This is a MacOS-only behavior. */
+ public fun hideCursorWhileTyping()
+}
+
+public val LocalPlatformCursorController: ProvidableCompositionLocal =
+ staticCompositionLocalOf {
+ error("No LocalPlatformCursorController provided. Have you forgotten the theme?")
+ }