From 93a1e863a329def5269a7ec732ae0331b739a8a2 Mon Sep 17 00:00:00 2001 From: Daniel Bertoldi Date: Thu, 5 Mar 2026 15:38:48 -0300 Subject: [PATCH] [JEWEL-1285] Update JDialogRenderer to Support Custom Window Shape add missing jbpopuprenderer to commits --- .../jewel/bridge/component/JBPopupRenderer.kt | 1 + .../intui/standalone/popup/JDialogRenderer.kt | 68 +++++++- platform/jewel/ui/api-dump.txt | 10 +- .../org/jetbrains/jewel/ui/component/Popup.kt | 153 +++++++++++++++++- 4 files changed, 222 insertions(+), 10 deletions(-) diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/component/JBPopupRenderer.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/component/JBPopupRenderer.kt index 6c99e89f025f3..271d3bc24968a 100644 --- a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/component/JBPopupRenderer.kt +++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/component/JBPopupRenderer.kt @@ -60,6 +60,7 @@ import org.jetbrains.jewel.foundation.LocalComponent import org.jetbrains.jewel.ui.component.PopupRenderer internal object JBPopupRenderer : PopupRenderer { + @Suppress("OVERRIDE_DEPRECATION") @Composable override fun Popup( popupPositionProvider: PopupPositionProvider, diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/popup/JDialogRenderer.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/popup/JDialogRenderer.kt index de746621d1612..96328007a8f7e 100644 --- a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/popup/JDialogRenderer.kt +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/popup/JDialogRenderer.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.popup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize @@ -94,6 +95,7 @@ import org.jetbrains.skiko.hostOs * possible issues. */ internal object JDialogRenderer : PopupRenderer { + @Suppress("OVERRIDE_DEPRECATION") @Composable override fun Popup( popupPositionProvider: PopupPositionProvider, @@ -103,6 +105,29 @@ internal object JDialogRenderer : PopupRenderer { onKeyEvent: ((KeyEvent) -> Boolean)?, cornerSize: CornerSize, content: @Composable () -> Unit, + ) { + Popup( + popupPositionProvider = popupPositionProvider, + properties = properties, + onDismissRequest = onDismissRequest, + onPreviewKeyEvent = onPreviewKeyEvent, + onKeyEvent = onKeyEvent, + cornerSize = cornerSize, + windowShape = null, + content = content, + ) + } + + @Composable + override fun Popup( + popupPositionProvider: PopupPositionProvider, + properties: PopupProperties, + onDismissRequest: (() -> Unit)?, + onPreviewKeyEvent: ((KeyEvent) -> Boolean)?, + onKeyEvent: ((KeyEvent) -> Boolean)?, + cornerSize: CornerSize, + windowShape: ((IntSize) -> java.awt.Shape)?, + content: @Composable () -> Unit, ) { val isJBREnvironment = remember { JBR.isAvailable() && JBR.isRoundedCornersManagerSupported() } val supportBlending = remember { @@ -148,6 +173,7 @@ internal object JDialogRenderer : PopupRenderer { onKeyEvent = onKeyEvent, cornerSize = cornerSize, blendingEnabled = supportBlending, + windowShape = windowShape, content = content, ) } @@ -164,6 +190,7 @@ private fun JPopupImpl( onKeyEvent: ((KeyEvent) -> Boolean)?, cornerSize: CornerSize, blendingEnabled: Boolean, + windowShape: ((IntSize) -> java.awt.Shape)?, content: @Composable () -> Unit, ) { val popupDensity = LocalDensity.current @@ -175,6 +202,7 @@ private fun JPopupImpl( val currentOnKeyEvent by rememberUpdatedState(onKeyEvent) val currentOnPreviewKeyEvent by rememberUpdatedState(onPreviewKeyEvent) val currentProperties by rememberUpdatedState(properties) + val windowShapeState = rememberUpdatedState(windowShape) val compositionLocalContext by rememberUpdatedState(currentCompositionLocalContext) @@ -242,19 +270,40 @@ private fun JPopupImpl( JPopupMeasurePolicy(dialog, currentPopupPositionProvider, parentBounds) { position, size -> popupRectangle = Rectangle(position.x, position.y, size.width, size.height) + val currentWindowShape = windowShapeState.value + if (currentWindowShape != null) { + if (blendingEnabled) { + // When blending is active (via compose.interop.blending), the window is + // already in java.awt.GraphicsDevice.WindowTranslucency.PERPIXEL_TRANSLUCENT + // mode (per-pixel alpha). Calling Window.setShape() would switch it to + // PERPIXEL_TRANSPARENT (hard pixel clip), breaking antialiasing at the edges. + // Compose's own drawing + transparent background is sufficient. + return@JPopupMeasurePolicy + } + // Without blending, fall back to Window.setShape() to at least + // clip the rectangular window boundary to the balloon outline. + // Note: this uses PERPIXEL_TRANSPARENT mode, which has known + // antialiasing limitations at concave corners (e.g. arrow junction). + val logicalSize = + IntSize( + floor(size.width / popupDensity.density).toInt(), + floor(size.height / popupDensity.density).toInt(), + ) + try { + dialog.shape = currentWindowShape(logicalSize) + } catch (_: UnsupportedOperationException) { + applyRoundedCorners(dialog, cornerSize, size, popupDensity) + } + return@JPopupMeasurePolicy + } + if (blendingEnabled) { // If any of the blending logic is enabled, we don't need to use JBR APIs // to set the rounded corners and fix the background. return@JPopupMeasurePolicy } - if (cornerSize != ZeroCornerSize) { - JBR.getRoundedCornersManager() - .setRoundedCorners( - dialog, - cornerSize.toPx(size.toSize(), popupDensity) / dialog.density(), - ) - } + applyRoundedCorners(dialog, cornerSize, size, popupDensity) } }, ) @@ -390,6 +439,11 @@ private class JPopupMeasurePolicy( } } +private fun applyRoundedCorners(dialog: Window, cornerSize: CornerSize, size: IntSize, density: Density) { + if (cornerSize == ZeroCornerSize) return + JBR.getRoundedCornersManager().setRoundedCorners(dialog, cornerSize.toPx(size.toSize(), density) / dialog.density()) +} + // Based on implementation from JBUIScale and ScreenUtil private fun IntSize.Companion.screenSize(window: Component): IntSize { val windowConfiguration = window.graphicsConfiguration.device.defaultConfiguration diff --git a/platform/jewel/ui/api-dump.txt b/platform/jewel/ui/api-dump.txt index e7d09117cd855..63da0f30ef192 100644 --- a/platform/jewel/ui/api-dump.txt +++ b/platform/jewel/ui/api-dump.txt @@ -573,8 +573,10 @@ f:org.jetbrains.jewel.ui.component.PopupContainerKt - bsf:PopupContainer(kotlin.jvm.functions.Function0,androidx.compose.ui.Alignment$Horizontal,androidx.compose.ui.Modifier,org.jetbrains.jewel.ui.component.styling.PopupContainerStyle,androidx.compose.ui.window.PopupProperties,androidx.compose.ui.window.PopupPositionProvider,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V - sf:PopupContainer(kotlin.jvm.functions.Function0,androidx.compose.ui.Alignment$Horizontal,androidx.compose.ui.Modifier,org.jetbrains.jewel.ui.component.styling.PopupContainerStyle,androidx.compose.ui.window.PopupProperties,androidx.compose.ui.window.PopupPositionProvider,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V f:org.jetbrains.jewel.ui.component.PopupKt -- sf:Popup(androidx.compose.ui.window.PopupPositionProvider,androidx.compose.foundation.shape.CornerSize,kotlin.jvm.functions.Function0,androidx.compose.ui.window.PopupProperties,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V -- sf:Popup(androidx.compose.ui.window.PopupPositionProvider,kotlin.jvm.functions.Function0,androidx.compose.ui.window.PopupProperties,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V +- sf:Popup(androidx.compose.ui.window.PopupPositionProvider,androidx.compose.foundation.shape.CornerSize,kotlin.jvm.functions.Function0,androidx.compose.ui.window.PopupProperties,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V +- bsf:Popup(androidx.compose.ui.window.PopupPositionProvider,androidx.compose.foundation.shape.CornerSize,kotlin.jvm.functions.Function0,androidx.compose.ui.window.PopupProperties,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V +- sf:Popup(androidx.compose.ui.window.PopupPositionProvider,kotlin.jvm.functions.Function0,androidx.compose.ui.window.PopupProperties,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V +- bsf:Popup(androidx.compose.ui.window.PopupPositionProvider,kotlin.jvm.functions.Function0,androidx.compose.ui.window.PopupProperties,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V - sf:getLocalPopupRenderer():androidx.compose.runtime.ProvidableCompositionLocal f:org.jetbrains.jewel.ui.component.PopupManager - sf:$stable:I @@ -590,8 +592,12 @@ f:org.jetbrains.jewel.ui.component.PopupManager - f:togglePopupVisibility():V org.jetbrains.jewel.ui.component.PopupRenderer - sf:Companion:org.jetbrains.jewel.ui.component.PopupRenderer$Companion +- Popup(androidx.compose.ui.window.PopupPositionProvider,androidx.compose.ui.window.PopupProperties,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,androidx.compose.foundation.shape.CornerSize,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I):V +- b:Popup(androidx.compose.ui.window.PopupPositionProvider,androidx.compose.ui.window.PopupProperties,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,androidx.compose.foundation.shape.CornerSize,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V - a:Popup(androidx.compose.ui.window.PopupPositionProvider,androidx.compose.ui.window.PopupProperties,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,androidx.compose.foundation.shape.CornerSize,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I):V f:org.jetbrains.jewel.ui.component.PopupRenderer$Companion +f:org.jetbrains.jewel.ui.component.PopupRenderer$ComposeDefaultImpls +- sf:Popup$default(androidx.compose.ui.window.PopupPositionProvider,androidx.compose.ui.window.PopupProperties,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,androidx.compose.foundation.shape.CornerSize,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,org.jetbrains.jewel.ui.component.PopupRenderer,androidx.compose.runtime.Composer,I,I):V f:org.jetbrains.jewel.ui.component.RadioButtonKt - sf:RadioButton(Z,kotlin.jvm.functions.Function0,androidx.compose.ui.Modifier,Z,org.jetbrains.jewel.ui.Outline,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.RadioButtonStyle,androidx.compose.ui.text.TextStyle,androidx.compose.ui.Alignment$Vertical,androidx.compose.runtime.Composer,I,I):V - bsf:RadioButtonRow(java.lang.String,Z,kotlin.jvm.functions.Function0,androidx.compose.ui.Modifier,Z,org.jetbrains.jewel.ui.Outline,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.RadioButtonStyle,androidx.compose.ui.text.TextStyle,androidx.compose.ui.Alignment$Vertical,androidx.compose.runtime.Composer,I,I):V diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Popup.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Popup.kt index cbd63c0718ee3..85062303720d8 100644 --- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Popup.kt +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Popup.kt @@ -46,22 +46,147 @@ import org.jetbrains.jewel.foundation.JewelFlags * consume the event. * @param onKeyEvent Callback invoked for key events after they are dispatched to children. Return `true` to consume the * event. + * @param content The composable content to be displayed inside the popup. + */ +@Deprecated(message = "Please use the overload with windowShape.", level = DeprecationLevel.HIDDEN) +@Composable +public fun Popup( + popupPositionProvider: PopupPositionProvider, + onDismissRequest: (() -> Unit)? = null, + properties: PopupProperties = PopupProperties(), + onPreviewKeyEvent: ((KeyEvent) -> Boolean)? = null, + onKeyEvent: ((KeyEvent) -> Boolean)? = null, + content: @Composable () -> Unit, +) { + Popup( + popupPositionProvider, + ZeroCornerSize, + onDismissRequest, + properties, + onPreviewKeyEvent, + onKeyEvent, + null, + content, + ) +} + +/** + * Displays a popup with the provided content at a position determined by the given [PopupPositionProvider]. + * + * This function behavior is influenced by the 'jewel.customPopupRender' system property. If set to `true`, it allows + * using a custom popup rendering implementation; otherwise, it defaults to the standard Compose popup. + * + * If running on the IntelliJ Platform and setting the [JewelFlags.useCustomPopupRenderer] property to `true`, the + * plugin will use the JBPopup implementation for rendering popups. This is useful if your composable content is small, + * but you need to display a popup that is bigger than the component size. + * + * @param popupPositionProvider Determines the position of the popup on the screen. + * @param onDismissRequest Callback invoked when a dismiss event is requested, typically when the popup is dismissed. + * @param properties Configuration parameters for the popup, such as whether it should consume touch events or focusable + * behavior. + * @param onPreviewKeyEvent Callback invoked for key events before they are dispatched to children. Return `true` to + * consume the event. + * @param onKeyEvent Callback invoked for key events after they are dispatched to children. Return `true` to consume the + * event. + * @param windowShape An optional factory that produces the [java.awt.Shape] used to clip the native popup window. The + * lambda receives the window's measured size in AWT logical units and must return a shape in the same coordinate + * system. Only applied by JDialogRenderer when `useCustomPopupRenderer = true`; all other renderers ignore it. When + * null, window clipping falls back to the `cornerSize`-based rounded corners (via JBR) if the platform supports it. + * @param content The composable content to be displayed inside the popup. + */ +@Composable +public fun Popup( + popupPositionProvider: PopupPositionProvider, + onDismissRequest: (() -> Unit)? = null, + properties: PopupProperties = PopupProperties(), + onPreviewKeyEvent: ((KeyEvent) -> Boolean)? = null, + onKeyEvent: ((KeyEvent) -> Boolean)? = null, + windowShape: ((IntSize) -> java.awt.Shape)? = null, + content: @Composable () -> Unit, +) { + Popup( + popupPositionProvider, + ZeroCornerSize, + onDismissRequest, + properties, + onPreviewKeyEvent, + onKeyEvent, + windowShape, + content, + ) +} + +/** + * Displays a popup with the provided content at a position determined by the given [PopupPositionProvider]. + * + * This function behavior is influenced by the 'jewel.customPopupRender' system property. If set to `true`, it allows + * using a custom popup rendering implementation; otherwise, it defaults to the standard Compose popup. + * + * If running on the IntelliJ Platform and setting the [JewelFlags.useCustomPopupRenderer] property to `true`, the + * plugin will use the JBPopup implementation for rendering popups. This is useful if your composable content is small, + * but you need to display a popup that is bigger than the component size. + * + * @param popupPositionProvider Determines the position of the popup on the screen. * @param cornerSize The size of the popup's rounded corners. This value gets ignored if the popup's implementation used * is the default Compose popup. + * @param onDismissRequest Callback invoked when a dismiss event is requested, typically when the popup is dismissed. + * @param properties Configuration parameters for the popup, such as whether it should consume touch events or focusable + * behavior. + * @param onPreviewKeyEvent Callback invoked for key events before they are dispatched to children. Return `true` to + * consume the event. + * @param onKeyEvent Callback invoked for key events after they are dispatched to children. Return `true` to consume the + * event. * @param content The composable content to be displayed inside the popup. */ +@Deprecated(message = "Please use the overload with windowShape.", level = DeprecationLevel.HIDDEN) @Composable public fun Popup( popupPositionProvider: PopupPositionProvider, + cornerSize: CornerSize, onDismissRequest: (() -> Unit)? = null, properties: PopupProperties = PopupProperties(), onPreviewKeyEvent: ((KeyEvent) -> Boolean)? = null, onKeyEvent: ((KeyEvent) -> Boolean)? = null, content: @Composable () -> Unit, ) { - Popup(popupPositionProvider, ZeroCornerSize, onDismissRequest, properties, onPreviewKeyEvent, onKeyEvent, content) + Popup( + popupPositionProvider, + cornerSize, + onDismissRequest, + properties, + onPreviewKeyEvent, + onKeyEvent, + windowShape = null, + content, + ) } +/** + * Displays a popup with the provided content at a position determined by the given [PopupPositionProvider]. + * + * This function behavior is influenced by the 'jewel.customPopupRender' system property. If set to `true`, it allows + * using a custom popup rendering implementation; otherwise, it defaults to the standard Compose popup. + * + * If running on the IntelliJ Platform and setting the [JewelFlags.useCustomPopupRenderer] property to `true`, the + * plugin will use the JBPopup implementation for rendering popups. This is useful if your composable content is small, + * but you need to display a popup that is bigger than the component size. + * + * @param popupPositionProvider Determines the position of the popup on the screen. + * @param cornerSize The size of the popup's rounded corners. This value gets ignored if the popup's implementation used + * is the default Compose popup. + * @param onDismissRequest Callback invoked when a dismiss event is requested, typically when the popup is dismissed. + * @param properties Configuration parameters for the popup, such as whether it should consume touch events or focusable + * behavior. + * @param onPreviewKeyEvent Callback invoked for key events before they are dispatched to children. Return `true` to + * consume the event. + * @param onKeyEvent Callback invoked for key events after they are dispatched to children. Return `true` to consume the + * event. + * @param windowShape An optional factory that produces the [java.awt.Shape] used to clip the native popup window. The + * lambda receives the window's measured size in AWT logical units and must return a shape in the same coordinate + * system. Only applied by JDialogRenderer when `useCustomPopupRenderer = true`; all other renderers ignore it. When + * null, window clipping falls back to the `cornerSize`-based rounded corners (via JBR) if the platform supports it. + * @param content The composable content to be displayed inside the popup. + */ @Composable public fun Popup( popupPositionProvider: PopupPositionProvider, @@ -70,6 +195,7 @@ public fun Popup( properties: PopupProperties = PopupProperties(), onPreviewKeyEvent: ((KeyEvent) -> Boolean)? = null, onKeyEvent: ((KeyEvent) -> Boolean)? = null, + windowShape: ((IntSize) -> java.awt.Shape)? = null, content: @Composable () -> Unit, ) { if (JewelFlags.useCustomPopupRenderer) { @@ -80,6 +206,7 @@ public fun Popup( onPreviewKeyEvent = onPreviewKeyEvent, onKeyEvent = onKeyEvent, cornerSize = cornerSize, + windowShape = windowShape, content = content, ) } else { @@ -102,6 +229,7 @@ public fun Popup( * [JewelFlags.useCustomPopupRenderer] flag to use it. */ public interface PopupRenderer { + @Deprecated(message = "Please use the overload with windowShape.") @Composable public fun Popup( popupPositionProvider: PopupPositionProvider, @@ -113,6 +241,28 @@ public interface PopupRenderer { content: @Composable () -> Unit, ) + @Composable + public fun Popup( + popupPositionProvider: PopupPositionProvider, + properties: PopupProperties, + onDismissRequest: (() -> Unit)?, + onPreviewKeyEvent: ((KeyEvent) -> Boolean)?, + onKeyEvent: ((KeyEvent) -> Boolean)?, + cornerSize: CornerSize, + windowShape: ((IntSize) -> java.awt.Shape)? = null, + content: @Composable () -> Unit, + ) { + Popup( + popupPositionProvider = popupPositionProvider, + properties = properties, + onDismissRequest = onDismissRequest, + onPreviewKeyEvent = onPreviewKeyEvent, + onKeyEvent = onKeyEvent, + cornerSize = cornerSize, + content = content, + ) + } + public companion object } @@ -126,6 +276,7 @@ public val LocalPopupRenderer: ProvidableCompositionLocal = stati } private object DefaultPopupRenderer : PopupRenderer { + @Suppress("OVERRIDE_DEPRECATION") @Composable override fun Popup( popupPositionProvider: PopupPositionProvider,