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..83eda27710324 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 @@ -68,6 +68,7 @@ internal object JBPopupRenderer : PopupRenderer { onPreviewKeyEvent: ((KeyEvent) -> Boolean)?, onKeyEvent: ((KeyEvent) -> Boolean)?, cornerSize: CornerSize, + windowShape: ((IntSize) -> java.awt.Shape)?, content: @Composable () -> Unit, ) { JBPopup( 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..fbcc66d9e52c8 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 @@ -102,6 +103,7 @@ internal object JDialogRenderer : PopupRenderer { onPreviewKeyEvent: ((KeyEvent) -> Boolean)?, onKeyEvent: ((KeyEvent) -> Boolean)?, cornerSize: CornerSize, + windowShape: ((IntSize) -> java.awt.Shape)?, content: @Composable () -> Unit, ) { val isJBREnvironment = remember { JBR.isAvailable() && JBR.isRoundedCornersManagerSupported() } @@ -148,6 +150,7 @@ internal object JDialogRenderer : PopupRenderer { onKeyEvent = onKeyEvent, cornerSize = cornerSize, blendingEnabled = supportBlending, + windowShape = windowShape, content = content, ) } @@ -164,6 +167,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 +179,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 +247,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 +416,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..06b4db48d33be 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,10 @@ 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 -- 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 +- 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.Function1,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..08734037bec85 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,158 @@ 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) + if (JewelFlags.useCustomPopupRenderer) { + LocalPopupRenderer.current.Popup( + popupPositionProvider = popupPositionProvider, + properties = properties, + onDismissRequest = onDismissRequest, + onPreviewKeyEvent = onPreviewKeyEvent, + onKeyEvent = onKeyEvent, + cornerSize = cornerSize, + windowShape = null, + content = content, + ) + } else { + ComposePopup( + popupPositionProvider = popupPositionProvider, + onDismissRequest = onDismissRequest, + properties = properties, + onPreviewKeyEvent = onPreviewKeyEvent, + onKeyEvent = onKeyEvent, + content = 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 +206,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 +217,7 @@ public fun Popup( onPreviewKeyEvent = onPreviewKeyEvent, onKeyEvent = onKeyEvent, cornerSize = cornerSize, + windowShape = windowShape, content = content, ) } else { @@ -110,6 +248,7 @@ public interface PopupRenderer { onPreviewKeyEvent: ((KeyEvent) -> Boolean)?, onKeyEvent: ((KeyEvent) -> Boolean)?, cornerSize: CornerSize, + windowShape: ((IntSize) -> java.awt.Shape)? = null, content: @Composable () -> Unit, ) @@ -134,6 +273,7 @@ private object DefaultPopupRenderer : PopupRenderer { onPreviewKeyEvent: ((KeyEvent) -> Boolean)?, onKeyEvent: ((KeyEvent) -> Boolean)?, cornerSize: CornerSize, + windowShape: ((IntSize) -> java.awt.Shape)?, content: @Composable () -> Unit, ) { ComposePopup(