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?") + }