Skip to content

Commit 14f668f

Browse files
committed
fix(tooltip): cancel pending delay on exit and fix popup positioning
- Cancel pending hover delay job when pointer exits to prevent tooltip from appearing after cursor leaves - Use PopupPositionProvider to position tooltip above anchor instead of overlapping it (centers horizontally, maintains 4dp gap) - Replace coroutineScope+launch with rememberCoroutineScope for proper job lifecycle management
1 parent 1fe9335 commit 14f668f

1 file changed

Lines changed: 42 additions & 23 deletions

File tree

  • macosui/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/components

macosui/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/components/Tooltip.kt

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,30 @@ import androidx.compose.runtime.Composable
1010
import androidx.compose.runtime.getValue
1111
import androidx.compose.runtime.mutableStateOf
1212
import androidx.compose.runtime.remember
13+
import androidx.compose.runtime.rememberCoroutineScope
1314
import androidx.compose.runtime.setValue
14-
import androidx.compose.ui.Alignment
1515
import androidx.compose.ui.ExperimentalComposeUiApi
1616
import androidx.compose.ui.Modifier
1717
import androidx.compose.ui.draw.clip
1818
import androidx.compose.ui.draw.shadow
1919
import androidx.compose.ui.graphics.graphicsLayer
20-
import androidx.compose.ui.input.pointer.PointerEventPass
2120
import androidx.compose.ui.input.pointer.PointerEventType
2221
import androidx.compose.ui.input.pointer.PointerType
2322
import androidx.compose.ui.input.pointer.pointerInput
23+
import androidx.compose.ui.platform.LocalDensity
2424
import androidx.compose.ui.text.TextStyle
2525
import androidx.compose.ui.tooling.preview.Preview
2626
import androidx.compose.ui.unit.IntOffset
27+
import androidx.compose.ui.unit.IntRect
28+
import androidx.compose.ui.unit.IntSize
29+
import androidx.compose.ui.unit.LayoutDirection
2730
import androidx.compose.ui.unit.dp
2831
import androidx.compose.ui.window.Popup
32+
import androidx.compose.ui.window.PopupPositionProvider
2933
import androidx.compose.ui.window.PopupProperties
3034
import io.github.kdroidfilter.nucleus.ui.apple.macos.components.Text
3135
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.MacosTheme
32-
import kotlinx.coroutines.coroutineScope
36+
import kotlinx.coroutines.Job
3337
import kotlinx.coroutines.delay
3438
import kotlinx.coroutines.launch
3539

@@ -61,39 +65,57 @@ fun Tooltip(
6165
val colors = MacosTheme.colorScheme
6266
val typography = MacosTheme.typography
6367
val shapes = MacosTheme.shapes
68+
val density = LocalDensity.current
69+
val scope = rememberCoroutineScope()
6470

6571
var showTooltip by remember { mutableStateOf(false) }
72+
var hoverJob: Job? by remember { mutableStateOf(null) }
6673

67-
// Fade animation
6874
val alpha by animateFloatAsState(
6975
targetValue = if (showTooltip) 1f else 0f,
7076
animationSpec = tween(durationMillis = 100),
7177
label = "tooltipAlpha",
7278
)
7379

74-
// Tooltip colors: inverted from current theme
7580
val tooltipBackground = colors.inverseSurface
76-
7781
val tooltipTextColor = colors.inverseOnSurface
7882

83+
val gapPx = with(density) { 4.dp.roundToPx() }
84+
val positionProvider = remember(gapPx) {
85+
object : PopupPositionProvider {
86+
override fun calculatePosition(
87+
anchorBounds: IntRect,
88+
windowSize: IntSize,
89+
layoutDirection: LayoutDirection,
90+
popupContentSize: IntSize,
91+
): IntOffset = IntOffset(
92+
x = (anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2)
93+
.coerceIn(0, (windowSize.width - popupContentSize.width).coerceAtLeast(0)),
94+
y = anchorBounds.top - popupContentSize.height - gapPx,
95+
)
96+
}
97+
}
98+
7999
Box(
80100
modifier = modifier
81101
.pointerInput(Unit) {
82-
coroutineScope {
83-
awaitPointerEventScope {
84-
val pass = PointerEventPass.Main
85-
while (true) {
86-
val event = awaitPointerEvent(pass)
87-
val inputType = event.changes.firstOrNull()?.type
88-
if (inputType == PointerType.Mouse) {
89-
when (event.type) {
90-
PointerEventType.Enter -> launch {
102+
awaitPointerEventScope {
103+
while (true) {
104+
val event = awaitPointerEvent()
105+
val inputType = event.changes.firstOrNull()?.type
106+
if (inputType == PointerType.Mouse) {
107+
when (event.type) {
108+
PointerEventType.Enter -> {
109+
hoverJob?.cancel()
110+
hoverJob = scope.launch {
91111
delay(delayMillis)
92112
showTooltip = true
93113
}
94-
PointerEventType.Exit -> {
95-
showTooltip = false
96-
}
114+
}
115+
PointerEventType.Exit -> {
116+
hoverJob?.cancel()
117+
hoverJob = null
118+
showTooltip = false
97119
}
98120
}
99121
}
@@ -105,11 +127,8 @@ fun Tooltip(
105127

106128
if (showTooltip || alpha > 0f) {
107129
Popup(
108-
alignment = Alignment.TopCenter,
109-
offset = IntOffset(0, -8),
110-
properties = PopupProperties(
111-
focusable = false,
112-
),
130+
popupPositionProvider = positionProvider,
131+
properties = PopupProperties(focusable = false),
113132
) {
114133
Box(
115134
modifier = Modifier

0 commit comments

Comments
 (0)