Skip to content

Instantly share code, notes, and snippets.

@OskarGroth
Created February 13, 2023 12:04
Show Gist options
  • Save OskarGroth/0872f4aee95e94417d336d99513128aa to your computer and use it in GitHub Desktop.
Save OskarGroth/0872f4aee95e94417d336d99513128aa to your computer and use it in GitHub Desktop.
import SwiftUI
public struct MouseTrackingView: View {
public var callbacks: NSMouseTrackingView.Callbacks
public var isEnabled: Bool
public var options: NSTrackingArea.Options
public var hitEnabled: Bool
public var flipYAxis: Bool
public init(mouseOverChanged: ((Bool) -> Void)? = nil, mouseMoved: ((CGPoint) -> Void)? = nil, mouseDown: (() -> Void)? = nil, mouseUp: ((Bool) -> Void)? = nil, isEnabled: Bool = true, trackingOptions: NSTrackingArea.Options = [.activeInKeyWindow, .enabledDuringMouseDrag, .mouseEnteredAndExited], hitEnabled: Bool = true, flipYAxis: Bool = false) {
self.callbacks = .init(mouseOverChanged: mouseOverChanged, mouseMoved: mouseMoved, mouseDown: mouseDown, mouseUp: mouseUp)
self.isEnabled = isEnabled
self.options = trackingOptions
self.hitEnabled = hitEnabled
self.flipYAxis = flipYAxis
}
public var body: some View {
Representable(
callbacks: callbacks,
isEnabled: isEnabled,
options: options,
hitEnabled: hitEnabled,
flipYAxis: flipYAxis
).accessibility(hidden: true)
}
}
extension MouseTrackingView {
struct Representable: NSViewRepresentable {
var callbacks: NSMouseTrackingView.Callbacks
var isEnabled: Bool
var options: NSTrackingArea.Options
var hitEnabled: Bool
var flipYAxis: Bool
func makeNSView(context: Context) -> NSMouseTrackingView {
context.coordinator.mouseTrackingView
}
func updateNSView(_ nsView: NSMouseTrackingView, context: Context) {
context.coordinator.update(callbacks: callbacks)
context.coordinator.update(isEnabled: isEnabled)
context.coordinator.update(options: options)
context.coordinator.update(hitEnabled: hitEnabled)
context.coordinator.update(flipYAxis: flipYAxis)
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
}
class Coordinator {
let mouseTrackingView = NSMouseTrackingView()
init() {
}
func update(flipYAxis: Bool) {
mouseTrackingView.flipYAxis = flipYAxis
}
func update(hitEnabled: Bool) {
mouseTrackingView.hitEnabled = hitEnabled
}
func update(options: NSTrackingArea.Options) {
mouseTrackingView.trackingOptions = options
}
func update(callbacks: NSMouseTrackingView.Callbacks) {
mouseTrackingView.callbacks = callbacks
}
func update(isEnabled: Bool) {
mouseTrackingView.isEnabled = isEnabled
}
}
}
public class NSMouseTrackingView: NSView {
public struct Callbacks {
public var mouseOverChanged: ((Bool) -> Void)?
public var mouseMoved: ((CGPoint) -> Void)?
public var mouseDown: (() -> Void)?
public var mouseUp: ((Bool) -> Void)?
}
public var callbacks = Callbacks()
override public var isFlipped: Bool {
return flipYAxis
}
public var flipYAxis: Bool = false
public var isEnabled: Bool = true
public var hitEnabled: Bool = true
public var trackingOptions: NSTrackingArea.Options = [.activeInKeyWindow, .mouseEnteredAndExited, .enabledDuringMouseDrag] {
didSet {
updateTrackingAreas()
}
}
private var trackingArea: NSTrackingArea?
public override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
commonInit()
}
fileprivate func commonInit() {
updateTrackingAreas()
}
override public func updateTrackingAreas() {
super.updateTrackingAreas()
if let trackingArea = trackingArea {
removeTrackingArea(trackingArea)
}
trackingArea = NSTrackingArea(rect: bounds, options: trackingOptions, owner: self, userInfo: nil)
addTrackingArea(trackingArea!)
}
public override func hitTest(_ point: NSPoint) -> NSView? {
guard callbacks.mouseDown != nil || callbacks.mouseUp != nil else {
return hitEnabled ? super.hitTest(point) : nil
}
return super.hitTest(point)
}
override public func mouseUp(with event: NSEvent) {
guard let callback = callbacks.mouseUp else { return } // propagate events
super.mouseUp(with: event)
guard isEnabled else { return }
let point = convert(event.locationInWindow, from: nil)
callback(trackingArea?.rect.contains(point) ?? false)
}
override public func mouseDown(with event: NSEvent) {
guard callbacks.mouseDown != nil || callbacks.mouseUp != nil else {
return // propagate. if we have mouseUp, we need to call super for mouseDown.
}
super.mouseDown(with: event)
guard let callback = callbacks.mouseDown else { return }
guard isEnabled else { return }
let point = convert(event.locationInWindow, from: nil)
if trackingArea?.rect.contains(point) ?? false {
callback()
}
}
override public func mouseDragged(with event: NSEvent) {
// propagate events
}
override public func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event)
guard isEnabled else { return }
callbacks.mouseOverChanged?(true)
}
override public func mouseExited(with event: NSEvent) {
super.mouseExited(with: event)
guard isEnabled else { return }
callbacks.mouseOverChanged?(false)
}
override public func mouseMoved(with event: NSEvent) {
super.mouseMoved(with: event)
guard isEnabled, let callback = callbacks.mouseMoved else { return }
let absolutePoint = convert(event.locationInWindow, from: nil)
// let relativePoint = CGPoint(x: absolutePoint.x / bounds.size.width, y: absolutePoint.y / bounds.size.height)
callback(absolutePoint)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment