From 3b5118fcbb7751faa23695fde973f21d0db6a49a Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Tue, 3 Feb 2026 16:04:33 +0200 Subject: [PATCH] Pre-compute tray position at click time to remove display latency The 250ms delay before showing the tray popup was needed because the position was computed in the LaunchedEffect, after the click had already propagated through coroutine dispatchers. Instead, compute the position directly in internalPrimaryAction at click time, when the native status item geometry is guaranteed to be available. The LaunchedEffect then uses this pre-computed position immediately without any delay. The polling fallback with delay is kept only for cases where no click occurred (initiallyVisible or programmatic show). --- .../kdroid/composetray/tray/api/TrayApp.kt | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt b/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt index a9f9bf0..2efec9f 100644 --- a/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt +++ b/src/commonMain/kotlin/com/kdroid/composetray/tray/api/TrayApp.kt @@ -508,6 +508,9 @@ private fun ApplicationScope.TrayAppImplOriginal( var lastFocusLostAt by remember { mutableStateOf(0L) } var autoHideEnabledAt by remember { mutableStateOf(0L) } + // Position pre-computed at click time so the LaunchedEffect can use it immediately. + var pendingPosition by remember { mutableStateOf(null) } + val dialogState = rememberDialogState(size = currentWindowSize) LaunchedEffect(currentWindowSize) { dialogState.size = currentWindowSize } @@ -541,6 +544,15 @@ private fun ApplicationScope.TrayAppImplOriginal( if (getOperatingSystem() == WINDOWS && (now - lastFocusLostAt) < 300) { // ignore immediate re-show after focus loss on Windows } else { + // Pre-compute position at click time: the native status item + // geometry is guaranteed to be available right now. + runCatching { + val widthPx = currentWindowSize.width.value.toInt() + val heightPx = currentWindowSize.height.value.toInt() + pendingPosition = getTrayWindowPositionForInstance( + tray.instanceKey(), widthPx, heightPx, horizontalOffset, verticalOffset + ) + } trayAppState.show() } } @@ -554,16 +566,25 @@ private fun ApplicationScope.TrayAppImplOriginal( if (isVisible) { if (!shouldShowWindow) { - delay(250) // let tray click/dock settle (macOS) - val widthPx = currentWindowSize.width.value.toInt() - val heightPx = currentWindowSize.height.value.toInt() - var position: WindowPosition = WindowPosition.PlatformDefault - val deadline = System.currentTimeMillis() + 3000 - while (position is WindowPosition.PlatformDefault && System.currentTimeMillis() < deadline) { - position = getTrayWindowPositionForInstance( - tray.instanceKey(), widthPx, heightPx, horizontalOffset, verticalOffset - ) + val preComputed = pendingPosition + pendingPosition = null + + val position = if (preComputed != null && preComputed !is WindowPosition.PlatformDefault) { + preComputed + } else { + // Fallback: poll for position (e.g. initiallyVisible or programmatic show) delay(250) + val widthPx = currentWindowSize.width.value.toInt() + val heightPx = currentWindowSize.height.value.toInt() + var pos: WindowPosition = WindowPosition.PlatformDefault + val deadline = System.currentTimeMillis() + 3000 + while (pos is WindowPosition.PlatformDefault && System.currentTimeMillis() < deadline) { + pos = getTrayWindowPositionForInstance( + tray.instanceKey(), widthPx, heightPx, horizontalOffset, verticalOffset + ) + if (pos is WindowPosition.PlatformDefault) delay(250) + } + pos } dialogState.position = position