fix: 用 NSEvent.addLocalMonitorForEvents 替代 .onHover/tracking area

之前的方案(.onHover 和 .background tracking area)都不可靠:
- .onHover 在 macOS 上行为不稳定
- .background() 里的 NSView 被 AVPlayerLayer 挡住收不到鼠标事件

新方案:用 NSEvent.addLocalMonitorForEvents 在应用层监听 mouseMoved 事件,
直接检测鼠标是否在窗口 frame 内,完全绕过 SwiftUI 的 hover 机制。
加上双层防抖(lastState + currentlyVisible 检查)避免重复设置。
This commit is contained in:
yumoqing 2026-07-01 07:48:16 +08:00
parent fde83a87bc
commit 16fdd72271

View File

@ -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