Skip to content

Commit

Permalink
Fix tooltips: metrics and positioning (#377)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rock3r authored May 7, 2024
1 parent 9f90c46 commit de5bbdb
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -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,
Expand Down
24 changes: 12 additions & 12 deletions ui/api/ui.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (JLandroidx/compose/ui/Alignment;FILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (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
}
Expand Down Expand Up @@ -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 <init> (JLandroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/unit/Density;FILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (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 {
Expand Down Expand Up @@ -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 <init> (Landroidx/compose/foundation/layout/PaddingValues;JLandroidx/compose/foundation/shape/CornerSize;FFJLandroidx/compose/ui/Alignment$Horizontal;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (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 {
Expand Down
125 changes: 46 additions & 79 deletions ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Tooltip.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -44,29 +45,26 @@ 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,
showDelay,
cornerSize,
borderWidth,
shadowSize,
tooltipOffset,
tooltipAlignment,
placement,
)
}
}
Expand Down

0 comments on commit de5bbdb

Please sign in to comment.