From d172b88349d632c6db21d3332ff9376dbce8df10 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 15 Mar 2026 13:00:29 +0530 Subject: [PATCH] BugFix: Hover Fix --- DynamicIsland/ContentView.swift | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 7c7a6b4..8909c88 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -150,6 +150,7 @@ struct ContentView: View { @State private var hoverClickMonitor: Any? @State private var hoverClickLocalMonitor: Any? @State private var stickyTerminalClickMonitor: Any? + @State private var hiddenEdgeHoverPollingTask: Task? @State private var gestureProgress: CGFloat = .zero @State private var skipGestureActiveDirection: MusicManager.SkipDirection? @@ -286,6 +287,13 @@ struct ContentView: View { hideNonNotchUntilHover && isNonNotchScreen && vm.notchState == .closed && !isSneakPeekVisibleOnCurrentScreen } + /// Whether the fallback top-edge hover detector should run. + /// This is only needed when the notch is fully hidden off-screen and + /// regular `.onHover` hit-testing may not trigger reliably. + private var shouldUseHiddenEdgeHoverPolling: Bool { + shouldHideUntilHover && !lockScreenManager.isLocked + } + /// Pill shape for Dynamic Island mode with animated corner radius transitions. private var currentPillShape: DynamicIslandPillShape { let radius: CGFloat @@ -535,6 +543,7 @@ struct ContentView: View { clearMusicControlVisibilityDeadline() } enqueueMusicControlWindowSync(forceRefresh: true) + startHiddenEdgeHoverPolling() } .onChange(of: vm.notchState) { _, state in if state == .open { @@ -626,6 +635,7 @@ struct ContentView: View { hoverTask?.cancel() stopHoverClickMonitor() removeStickyTerminalClickMonitor() + stopHiddenEdgeHoverPolling() cancelMusicControlWindowSync() hideMusicControlWindow() cancelMusicControlVisibilityTimer() @@ -1563,6 +1573,48 @@ struct ContentView: View { } } + private func hiddenHoverActivationContainsMouse(_ location: NSPoint = NSEvent.mouseLocation) -> Bool { + guard let screen = NSScreen.screens.first(where: { $0.localizedName == currentScreenName }) else { + return false + } + + let horizontalPadding: CGFloat = 8 + let activationWidth = vm.closedNotchSize.width + horizontalPadding * 2 + let activationHeight = max(vm.closedNotchSize.height + zeroHeightHoverPadding, 14) + let activationRect = CGRect( + x: screen.frame.midX - activationWidth / 2, + y: screen.frame.maxY - activationHeight, + width: activationWidth, + height: activationHeight + ) + + return activationRect.contains(location) + } + + private func startHiddenEdgeHoverPolling() { + guard hiddenEdgeHoverPollingTask == nil else { return } + + hiddenEdgeHoverPollingTask = Task { @MainActor in + while !Task.isCancelled { + if self.shouldUseHiddenEdgeHoverPolling { + let hovering = self.hiddenHoverActivationContainsMouse() + if hovering != self.isHovering { + self.handleHover(hovering) + } + } + + try? await Task.sleep(for: .milliseconds(50)) + } + + self.hiddenEdgeHoverPollingTask = nil + } + } + + private func stopHiddenEdgeHoverPolling() { + hiddenEdgeHoverPollingTask?.cancel() + hiddenEdgeHoverPollingTask = nil + } + private func startHoverClickMonitor() { guard hoverClickMonitor == nil else { return }