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 // MARK: - .onHover background tracking area
struct MouseTrackingArea: NSViewRepresentable { class WindowMouseMonitor {
let onHoverChange: @MainActor (Bool) -> Void static let shared = WindowMouseMonitor()
private var monitor: Any?
private var lastState: Bool?
func makeNSView(context: Context) -> MouseTrackingNSView { func startMonitoring() {
let view = MouseTrackingNSView() guard monitor == nil else { return }
view.onHoverChange = onHoverChange monitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { [weak self] event in
return view self?.handleMouseMoved(event)
return event
}
} }
func updateNSView(_ nsView: MouseTrackingNSView, context: Context) { private func handleMouseMoved(_ event: NSEvent) {
nsView.onHoverChange = onHoverChange 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 }
class MouseTrackingNSView: NSView { lastState = isInside
var onHoverChange: (@MainActor (Bool) -> Void)? NSLog("[MiniPlayer] WindowMouseMonitor: mouseInside=%d", isInside)
private var trackingArea: NSTrackingArea? //
private var isHovering = false let currentlyVisible = win.titleVisibility == .visible
guard currentlyVisible != isInside else { return }
override func updateTrackingAreas() { win.titlebarAppearsTransparent = !isInside
super.updateTrackingAreas() win.titleVisibility = isInside ? .visible : .hidden
if let existing = trackingArea { win.standardWindowButton(.closeButton)?.isHidden = !isInside
removeTrackingArea(existing) win.standardWindowButton(.miniaturizeButton)?.isHidden = !isInside
} win.standardWindowButton(.zoomButton)?.isHidden = !isInside
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)
} }
} }
@ -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 { var body: some View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
Color.black.ignoresSafeArea() Color.black.ignoresSafeArea()
@ -375,9 +350,6 @@ struct PlayerContentView: View {
} }
} }
} }
.background(MouseTrackingArea(onHoverChange: { hovering in
setWindowTitleBarVisible(hovering)
}))
.onAppear { .onAppear {
// 绿 // 绿
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
@ -388,6 +360,12 @@ struct PlayerContentView: View {
win.standardWindowButton(.closeButton)?.isHidden = true win.standardWindowButton(.closeButton)?.isHidden = true
win.standardWindowButton(.miniaturizeButton)?.isHidden = true win.standardWindowButton(.miniaturizeButton)?.isHidden = true
win.standardWindowButton(.zoomButton)?.isHidden = true win.standardWindowButton(.zoomButton)?.isHidden = true
// mouseMoved
win.acceptsMouseMovedEvents = true
// .onHover
WindowMouseMonitor.shared.startMonitoring()
} }
} }
.onChange(of: showToolbar) { _, newValue in .onChange(of: showToolbar) { _, newValue in