From de5bbdb91ee86518aa07fde97d6715b1e3321933 Mon Sep 17 00:00:00 2001 From: Sebastiano Poggi Date: Tue, 7 May 2024 07:53:41 +0100 Subject: [PATCH] Fix tooltips: metrics and positioning (#377) This fixes the tooltip metrics defaults to something looking more like the IJP's, both in terms of delay and of behaviour. We now use a simplified position calculation, that is less complex than the custom logic we had both in terms of behaviour and in terms of code. It mostly piggybacks the built-in PopupPositionProviderAtPosition, but with a twist of never moving after being shown (like Swing tooltips do in the IJP). We also set a sensible delay, matching the Swing defaults of 1.2s by default. In the bridge, we read as many metrics properties as possible from the LaF/Registry. --- .../jewel/bridge/theme/IntUiBridge.kt | 14 +- .../samples/standalone/view/ComponentsView.kt | 14 +- ui/api/ui.api | 24 ++-- .../jetbrains/jewel/ui/component/Tooltip.kt | 125 +++++++----------- .../ui/component/styling/TooltipStyling.kt | 16 +-- 5 files changed, 85 insertions(+), 108 deletions(-) diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt index 3980858fd..e24936a83 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt @@ -17,6 +17,7 @@ import com.intellij.ide.ui.LafManager import com.intellij.ide.ui.laf.darcula.DarculaUIUtil import com.intellij.ide.ui.laf.intellij.IdeaPopupMenuUI import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.util.registry.Registry import com.intellij.ui.JBColor import com.intellij.ui.NewUI import com.intellij.util.ui.DirProvider @@ -1013,16 +1014,21 @@ private fun readCircularProgressStyle(isDark: Boolean) = .takeOrElse { if (isDark) Color(0xFF6F737A) else Color(0xFFA8ADBD) }, ) -private fun readTooltipStyle() = - TooltipStyle( - metrics = TooltipMetrics.defaults(), +private fun readTooltipStyle(): TooltipStyle { + return TooltipStyle( + metrics = TooltipMetrics.defaults( + contentPadding = JBUI.CurrentTheme.HelpTooltip.smallTextBorderInsets().toPaddingValues(), + showDelay = Registry.intValue("ide.tooltip.initialDelay").milliseconds, + cornerSize = CornerSize(JBUI.CurrentTheme.Tooltip.CORNER_RADIUS.dp), + ), colors = TooltipColors( content = retrieveColorOrUnspecified("ToolTip.foreground"), background = retrieveColorOrUnspecified("ToolTip.background"), - border = retrieveColorOrUnspecified("ToolTip.borderColor"), + border = JBUI.CurrentTheme.Tooltip.borderColor().toComposeColor(), shadow = retrieveColorOrUnspecified("Notification.Shadow.bottom1Color"), ), ) +} private fun readIconButtonStyle(): IconButtonStyle = IconButtonStyle( diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/ComponentsView.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/ComponentsView.kt index 3aa05536a..3c24ef186 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/ComponentsView.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/ComponentsView.kt @@ -1,5 +1,6 @@ package org.jetbrains.jewel.samples.standalone.view +import androidx.compose.foundation.TooltipPlacement import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -15,8 +16,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import org.jetbrains.jewel.foundation.modifier.trackActivation import org.jetbrains.jewel.foundation.theme.JewelTheme @@ -30,12 +29,15 @@ import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.SelectableIconButton import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.Tooltip -import org.jetbrains.jewel.ui.component.TooltipPlacement import org.jetbrains.jewel.ui.component.Typography import org.jetbrains.jewel.ui.component.styling.LocalIconButtonStyle +import org.jetbrains.jewel.ui.component.styling.TooltipMetrics +import org.jetbrains.jewel.ui.component.styling.TooltipStyle import org.jetbrains.jewel.ui.painter.hints.Size import org.jetbrains.jewel.ui.painter.hints.Stroke import org.jetbrains.jewel.ui.painter.rememberResourcePainterProvider +import org.jetbrains.jewel.ui.theme.tooltipStyle +import kotlin.time.Duration.Companion.milliseconds @Composable @View(title = "Components", position = 1, icon = "icons/structure.svg") @@ -53,7 +55,11 @@ fun ComponentsToolBar() { ComponentsViewModel.views.forEach { Tooltip( tooltip = { Text("Show ${it.title}") }, - tooltipPlacement = TooltipPlacement(DpOffset(40.dp, 0.dp), Alignment.End, LocalDensity.current), + style = TooltipStyle( + JewelTheme.tooltipStyle.colors, + TooltipMetrics.defaults(showDelay = 150.milliseconds), + ), + tooltipPlacement = TooltipPlacement.ComponentRect(Alignment.CenterEnd, Alignment.CenterEnd), ) { SelectableIconButton( selected = ComponentsViewModel.currentView == it, diff --git a/ui/api/ui.api b/ui/api/ui.api index a5f24c312..a13007b19 100644 --- a/ui/api/ui.api +++ b/ui/api/ui.api @@ -291,6 +291,13 @@ public final class org/jetbrains/jewel/ui/component/DropdownState$Companion { public static synthetic fun of-17HSnUM$default (Lorg/jetbrains/jewel/ui/component/DropdownState$Companion;ZZZZZILjava/lang/Object;)J } +public final class org/jetbrains/jewel/ui/component/FixedCursorPoint : androidx/compose/foundation/TooltipPlacement { + public static final field $stable I + public synthetic fun (JLandroidx/compose/ui/Alignment;FILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JLandroidx/compose/ui/Alignment;FLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun positionProvider-9KIMszo (JLandroidx/compose/runtime/Composer;I)Landroidx/compose/ui/window/PopupPositionProvider; +} + public final class org/jetbrains/jewel/ui/component/GroupHeaderKt { public static final fun GroupHeader-cf5BqRc (Ljava/lang/String;Landroidx/compose/ui/Modifier;JLorg/jetbrains/jewel/ui/component/styling/GroupHeaderStyle;Landroidx/compose/runtime/Composer;II)V } @@ -731,13 +738,7 @@ public final class org/jetbrains/jewel/ui/component/TextKt { public final class org/jetbrains/jewel/ui/component/TooltipKt { public static final fun Tooltip (Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;Lorg/jetbrains/jewel/ui/component/styling/TooltipStyle;Landroidx/compose/foundation/TooltipPlacement;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V -} - -public final class org/jetbrains/jewel/ui/component/TooltipPlacement : androidx/compose/foundation/TooltipPlacement { - public static final field $stable I - public synthetic fun (JLandroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/unit/Density;FILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (JLandroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/unit/Density;FLkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun positionProvider-9KIMszo (JLandroidx/compose/runtime/Composer;I)Landroidx/compose/ui/window/PopupPositionProvider; + public static final fun rememberPopupPositionProviderAtFixedPosition-7KAyTs4 (JJLandroidx/compose/ui/Alignment;FLandroidx/compose/runtime/Composer;II)Landroidx/compose/ui/window/PopupPositionProvider; } public final class org/jetbrains/jewel/ui/component/Typography { @@ -2074,22 +2075,21 @@ public final class org/jetbrains/jewel/ui/component/styling/TooltipColors$Compan public final class org/jetbrains/jewel/ui/component/styling/TooltipMetrics { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/TooltipMetrics$Companion; - public synthetic fun (Landroidx/compose/foundation/layout/PaddingValues;JLandroidx/compose/foundation/shape/CornerSize;FFJLandroidx/compose/ui/Alignment$Horizontal;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Landroidx/compose/foundation/layout/PaddingValues;JLandroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/TooltipPlacement;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getBorderWidth-D9Ej5fM ()F public final fun getContentPadding ()Landroidx/compose/foundation/layout/PaddingValues; public final fun getCornerSize ()Landroidx/compose/foundation/shape/CornerSize; + public final fun getPlacement ()Landroidx/compose/foundation/TooltipPlacement; public final fun getShadowSize-D9Ej5fM ()F public final fun getShowDelay-UwyO8pc ()J - public final fun getTooltipAlignment ()Landroidx/compose/ui/Alignment$Horizontal; - public final fun getTooltipOffset-RKDOV3M ()J public fun hashCode ()I public fun toString ()Ljava/lang/String; } public final class org/jetbrains/jewel/ui/component/styling/TooltipMetrics$Companion { - public final fun defaults-e4GK2sY (Landroidx/compose/foundation/layout/PaddingValues;JLandroidx/compose/foundation/shape/CornerSize;FFJLandroidx/compose/ui/Alignment$Horizontal;)Lorg/jetbrains/jewel/ui/component/styling/TooltipMetrics; - public static synthetic fun defaults-e4GK2sY$default (Lorg/jetbrains/jewel/ui/component/styling/TooltipMetrics$Companion;Landroidx/compose/foundation/layout/PaddingValues;JLandroidx/compose/foundation/shape/CornerSize;FFJLandroidx/compose/ui/Alignment$Horizontal;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/TooltipMetrics; + public final fun defaults-8qf-r9M (Landroidx/compose/foundation/layout/PaddingValues;JLandroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/TooltipPlacement;)Lorg/jetbrains/jewel/ui/component/styling/TooltipMetrics; + public static synthetic fun defaults-8qf-r9M$default (Lorg/jetbrains/jewel/ui/component/styling/TooltipMetrics$Companion;Landroidx/compose/foundation/layout/PaddingValues;JLandroidx/compose/foundation/shape/CornerSize;FFLandroidx/compose/foundation/TooltipPlacement;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/TooltipMetrics; } public final class org/jetbrains/jewel/ui/component/styling/TooltipStyle { diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Tooltip.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Tooltip.kt index c7766bb3d..b347423eb 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Tooltip.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Tooltip.kt @@ -16,15 +16,12 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntRect -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupPositionProviderAtPosition +import org.jetbrains.jewel.foundation.ExperimentalJewelApi import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.foundation.theme.LocalContentColor import org.jetbrains.jewel.foundation.theme.OverrideDarkMode @@ -37,11 +34,7 @@ public fun Tooltip( tooltip: @Composable () -> Unit, modifier: Modifier = Modifier, style: TooltipStyle = JewelTheme.tooltipStyle, - tooltipPlacement: TooltipPlacement = TooltipPlacement( - contentOffset = style.metrics.tooltipOffset, - alignment = style.metrics.tooltipAlignment, - density = LocalDensity.current, - ), + tooltipPlacement: TooltipPlacement = style.metrics.placement, content: @Composable () -> Unit, ) { TooltipArea( @@ -81,86 +74,60 @@ public fun Tooltip( ) } -public class TooltipPlacement( - private val contentOffset: DpOffset, - private val alignment: Alignment.Horizontal, - private val density: Density, +/** + * [TooltipPlacement] implementation for providing a [PopupPositionProvider] that calculates + * the position of the popup relative to the current mouse cursor position, but never changes + * it after showing the popup. + * + * @param offset [DpOffset] to be added to the position of the popup. + * @param alignment The alignment of the popup relative to the current cursor position. + * @param windowMargin Defines the area within the window that limits the placement of the popup. + */ +@ExperimentalJewelApi +public class FixedCursorPoint( + private val offset: DpOffset = DpOffset.Zero, + private val alignment: Alignment = Alignment.BottomEnd, private val windowMargin: Dp = 4.dp, ) : TooltipPlacement { - @Composable - public override fun positionProvider(cursorPosition: Offset): PopupPositionProvider = - rememberTooltipPositionProvider( - cursorPosition = cursorPosition, - contentOffset = contentOffset, + override fun positionProvider(cursorPosition: Offset): PopupPositionProvider = + rememberPopupPositionProviderAtFixedPosition( + positionPx = cursorPosition, + offset = offset, alignment = alignment, - density = density, windowMargin = windowMargin, ) } +/** + * A [PopupPositionProvider] that positions the popup at the given position relative to the anchor, + * but never updates it after showing the popup. + * + * @param positionPx the offset, in pixels, relative to the anchor, to position the popup at. + * @param offset [DpOffset] to be added to the position of the popup. + * @param alignment The alignment of the popup relative to desired position. + * @param windowMargin Defines the area within the window that limits the placement of the popup. + */ +@ExperimentalJewelApi @Composable -private fun rememberTooltipPositionProvider( - cursorPosition: Offset, - contentOffset: DpOffset, - alignment: Alignment.Horizontal, - density: Density, +public fun rememberPopupPositionProviderAtFixedPosition( + positionPx: Offset, + offset: DpOffset = DpOffset.Zero, + alignment: Alignment = Alignment.BottomEnd, windowMargin: Dp = 4.dp, -) = - remember(contentOffset, alignment, density, windowMargin) { - TooltipPositionProvider( - cursorPosition = cursorPosition, - contentOffset = contentOffset, +): PopupPositionProvider = with(LocalDensity.current) { + val offsetPx = Offset(offset.x.toPx(), offset.y.toPx()) + val windowMarginPx = windowMargin.roundToPx() + + val initialPosition = remember { positionPx } + + remember(offsetPx, alignment, windowMarginPx) { + PopupPositionProviderAtPosition( + positionPx = initialPosition, + isRelativeToAnchor = true, + offsetPx = offsetPx, alignment = alignment, - density = density, - windowMargin = windowMargin, + windowMarginPx = windowMarginPx, ) } - -private class TooltipPositionProvider( - private val cursorPosition: Offset, - private val contentOffset: DpOffset, - private val alignment: Alignment.Horizontal, - private val density: Density, - private val windowMargin: Dp = 4.dp, -) : PopupPositionProvider { - - override fun calculatePosition( - anchorBounds: IntRect, - windowSize: IntSize, - layoutDirection: LayoutDirection, - popupContentSize: IntSize, - ): IntOffset = - with(density) { - val windowSpaceBounds = IntRect( - left = windowMargin.roundToPx(), - top = windowMargin.roundToPx(), - right = windowSize.width - windowMargin.roundToPx(), - bottom = windowSize.height - windowMargin.roundToPx(), - ) - - val contentOffsetX = contentOffset.x.roundToPx() - val contentOffsetY = contentOffset.y.roundToPx() - - val posX = cursorPosition.x.toInt() + anchorBounds.left - val posY = cursorPosition.y.toInt() + anchorBounds.top - - val x = posX + alignment.align(popupContentSize.width, 0, layoutDirection) + contentOffsetX - - val aboveSpacing = cursorPosition.y - contentOffsetY - windowSpaceBounds.top - val belowSpacing = windowSpaceBounds.bottom - cursorPosition.y - contentOffsetY - - val y = - if (belowSpacing > popupContentSize.height || belowSpacing >= aboveSpacing) { - posY + contentOffsetY - } else { - posY - contentOffsetY - popupContentSize.height - } - - val popupBounds = - IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height) - .constrainedIn(windowSpaceBounds) - - IntOffset(popupBounds.left, popupBounds.top) - } } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TooltipStyling.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TooltipStyling.kt index d80c4b4fa..0190ba1d0 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TooltipStyling.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TooltipStyling.kt @@ -1,16 +1,17 @@ package org.jetbrains.jewel.ui.component.styling +import androidx.compose.foundation.TooltipPlacement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CornerSize import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import org.jetbrains.jewel.foundation.GenerateDataFunctions +import org.jetbrains.jewel.ui.component.FixedCursorPoint import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -44,20 +45,18 @@ public class TooltipMetrics( public val cornerSize: CornerSize, public val borderWidth: Dp, public val shadowSize: Dp, - public val tooltipOffset: DpOffset, - public val tooltipAlignment: Alignment.Horizontal, + public val placement: TooltipPlacement, ) { public companion object { public fun defaults( contentPadding: PaddingValues = PaddingValues(vertical = 9.dp, horizontal = 12.dp), - showDelay: Duration = 0.milliseconds, - cornerSize: CornerSize = CornerSize(5.dp), + showDelay: Duration = 1200.milliseconds, + cornerSize: CornerSize = CornerSize(4.dp), borderWidth: Dp = 1.dp, shadowSize: Dp = 12.dp, - tooltipOffset: DpOffset = DpOffset(0.dp, 20.dp), - tooltipAlignment: Alignment.Horizontal = Alignment.Start, + placement: TooltipPlacement = FixedCursorPoint(DpOffset(4.dp, 24.dp)), ): TooltipMetrics = TooltipMetrics( contentPadding, @@ -65,8 +64,7 @@ public class TooltipMetrics( cornerSize, borderWidth, shadowSize, - tooltipOffset, - tooltipAlignment, + placement, ) } }