import AppKitimport SwiftUI /// Lightweight NSView-based mouse tracking with local coordinates.////// Why: SwiftUI's `onHover` doesn't provide location, but we want "hover a bar to see values" on macOS.@MainActorstruct MouseLocationReader: NSViewRepresentable { let onMoved: (CGPoint?) -> Void func makeNSView(context: Context) -> TrackingView { let view = TrackingView() view.onMoved = self.onMoved return view } func updateNSView(_ nsView: TrackingView, context: Context) { nsView.onMoved = self.onMoved } final class TrackingView: NSView { var onMoved: ((CGPoint?) -> Void)? private var trackingArea: NSTrackingArea? override func viewDidMoveToWindow() { super.viewDidMoveToWindow() self.window?.acceptsMouseMovedEvents = true self.updateTrackingAreas() } override func updateTrackingAreas() { super.updateTrackingAreas() if let trackingArea { self.removeTrackingArea(trackingArea) } let options: NSTrackingArea.Options = [ // NSMenu popups aren't "key windows", so `.activeInKeyWindow` would drop events and cause hover // state to flicker. `.activeAlways` keeps tracking stable while the menu is open. .activeAlways, .inVisibleRect, .mouseEnteredAndExited, .mouseMoved, ] let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil) self.addTrackingArea(area) self.trackingArea = area } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) self.onMoved?(self.convert(event.locationInWindow, from: nil)) } override func mouseMoved(with event: NSEvent) { super.mouseMoved(with: event) self.onMoved?(self.convert(event.locationInWindow, from: nil)) } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) self.onMoved?(nil) } }}
import AppKit
import SwiftUI
/// Lightweight NSView-based mouse tracking with local coordinates.
///
/// Why: SwiftUI's `onHover` doesn't provide location, but we want "hover a bar to see values" on macOS.
@MainActor
struct MouseLocationReader: NSViewRepresentable {
let onMoved: (CGPoint?) -> Void
func makeNSView(context: Context) -> TrackingView {
let view = TrackingView()
view.onMoved = self.onMoved
return view
}
func updateNSView(_ nsView: TrackingView, context: Context) {
nsView.onMoved = self.onMoved
}
final class TrackingView: NSView {
var onMoved: ((CGPoint?) -> Void)?
private var trackingArea: NSTrackingArea?
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
self.window?.acceptsMouseMovedEvents = true
self.updateTrackingAreas()
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
if let trackingArea {
self.removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [
// NSMenu popups aren't "key windows", so `.activeInKeyWindow` would drop events and cause hover
// state to flicker. `.activeAlways` keeps tracking stable while the menu is open.
.activeAlways,
.inVisibleRect,
.mouseEnteredAndExited,
.mouseMoved,
]
let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil)
self.addTrackingArea(area)
self.trackingArea = area
}
override func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event)
self.onMoved?(self.convert(event.locationInWindow, from: nil))
}
override func mouseMoved(with event: NSEvent) {
super.mouseMoved(with: event)
self.onMoved?(self.convert(event.locationInWindow, from: nil))
}
override func mouseExited(with event: NSEvent) {
super.mouseExited(with: event)
self.onMoved?(nil)
}
}
}