From 16fdd72271fe88e21186ad48c482c6e43a82fe47 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 1 Jul 2026 07:48:16 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=94=A8=20NSEvent.addLocalMonitorForEv?= =?UTF-8?q?ents=20=E6=9B=BF=E4=BB=A3=20.onHover/tracking=20area?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前的方案(.onHover 和 .background tracking area)都不可靠: - .onHover 在 macOS 上行为不稳定 - .background() 里的 NSView 被 AVPlayerLayer 挡住收不到鼠标事件 新方案:用 NSEvent.addLocalMonitorForEvents 在应用层监听 mouseMoved 事件, 直接检测鼠标是否在窗口 frame 内,完全绕过 SwiftUI 的 hover 机制。 加上双层防抖(lastState + currentlyVisible 检查)避免重复设置。 --- Sources/MiniPlayerApp.swift | 84 ++++++++++++++----------------------- 1 file changed, 31 insertions(+), 53 deletions(-) diff --git a/Sources/MiniPlayerApp.swift b/Sources/MiniPlayerApp.swift index 1eaf3eb..e43b15e 100644 --- a/Sources/MiniPlayerApp.swift +++ b/Sources/MiniPlayerApp.swift @@ -223,51 +223,35 @@ class PlayerLayerHostView: NSView { } } -// MARK: - 可靠的鼠标追踪(替代 SwiftUI .onHover 的不可靠行为) -struct MouseTrackingArea: NSViewRepresentable { - let onHoverChange: @MainActor (Bool) -> Void +// MARK: - 窗口级鼠标监控(替代 .onHover 和 background tracking area) +class WindowMouseMonitor { + static let shared = WindowMouseMonitor() + private var monitor: Any? + private var lastState: Bool? - func makeNSView(context: Context) -> MouseTrackingNSView { - let view = MouseTrackingNSView() - view.onHoverChange = onHoverChange - return view - } - - func updateNSView(_ nsView: MouseTrackingNSView, context: Context) { - nsView.onHoverChange = onHoverChange - } -} - -class MouseTrackingNSView: NSView { - var onHoverChange: (@MainActor (Bool) -> Void)? - private var trackingArea: NSTrackingArea? - private var isHovering = false - - override func updateTrackingAreas() { - super.updateTrackingAreas() - if let existing = trackingArea { - removeTrackingArea(existing) + func startMonitoring() { + guard monitor == nil else { return } + monitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { [weak self] event in + self?.handleMouseMoved(event) + return event } - let area = NSTrackingArea( - rect: .zero, - options: [.mouseEnteredAndExited, .activeInKeyWindow, .inVisibleRect], - owner: self, - userInfo: nil - ) - addTrackingArea(area) - trackingArea = area } - override func mouseEntered(with event: NSEvent) { - guard !isHovering else { return } - isHovering = true - onHoverChange?(true) - } - - override func mouseExited(with event: NSEvent) { - guard isHovering else { return } - isHovering = false - onHoverChange?(false) + private func handleMouseMoved(_ event: NSEvent) { + guard let win = NSApplication.shared.windows.first(where: { $0.isVisible && !($0 is NSPanel) }) else { return } + let mouseScreenPos = NSEvent.mouseLocation + let isInside = win.frame.contains(mouseScreenPos) + guard isInside != lastState else { return } + lastState = isInside + NSLog("[MiniPlayer] WindowMouseMonitor: mouseInside=%d", isInside) + // 防抖:状态没变就不动 + let currentlyVisible = win.titleVisibility == .visible + guard currentlyVisible != isInside else { return } + win.titlebarAppearsTransparent = !isInside + win.titleVisibility = isInside ? .visible : .hidden + win.standardWindowButton(.closeButton)?.isHidden = !isInside + win.standardWindowButton(.miniaturizeButton)?.isHidden = !isInside + win.standardWindowButton(.zoomButton)?.isHidden = !isInside } } @@ -289,15 +273,6 @@ struct PlayerContentView: View { } } - private func setWindowTitleBarVisible(_ visible: Bool) { - guard let win = NSApplication.shared.windows.first(where: { $0.isVisible }) else { return } - win.titlebarAppearsTransparent = !visible - win.titleVisibility = visible ? .visible : .hidden - win.standardWindowButton(.closeButton)?.isHidden = !visible - win.standardWindowButton(.miniaturizeButton)?.isHidden = !visible - win.standardWindowButton(.zoomButton)?.isHidden = !visible - } - var body: some View { ZStack(alignment: .topLeading) { Color.black.ignoresSafeArea() @@ -375,9 +350,6 @@ struct PlayerContentView: View { } } } - .background(MouseTrackingArea(onHoverChange: { hovering in - setWindowTitleBarVisible(hovering) - })) .onAppear { // 初始状态:标题栏完全隐藏(含红绿灯按钮) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -388,6 +360,12 @@ struct PlayerContentView: View { win.standardWindowButton(.closeButton)?.isHidden = true win.standardWindowButton(.miniaturizeButton)?.isHidden = true win.standardWindowButton(.zoomButton)?.isHidden = true + + // 启用 mouseMoved 事件(默认不发送) + win.acceptsMouseMovedEvents = true + + // 用全局鼠标事件监听器替代 .onHover + WindowMouseMonitor.shared.startMonitoring() } } .onChange(of: showToolbar) { _, newValue in