← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


ListlessMac/Helpers/BackgroundClickMonitor.swift (1976B)

1import AppKit
2import SwiftUI
3
4/// Monitors for mouse clicks in empty scroll view space (below content).
5///
6/// SwiftUI's `.contentShape(Rectangle()).onTapGesture` on a `ScrollView`
7/// does not reliably fire for clicks in the area below the document view
8/// on macOS, because `NSScrollView`'s clip view handles the event before
9/// SwiftUI's gesture system. This representable installs a local event
10/// monitor that detects those clicks and forwards them to a handler.
11struct BackgroundClickMonitor: NSViewRepresentable {
12    let onClick: () -> Void
13
14    func makeNSView(context: Context) -> ClickMonitorNSView {
15        let view = ClickMonitorNSView()
16        view.onClick = onClick
17        return view
18    }
19
20    func updateNSView(_ nsView: ClickMonitorNSView, context: Context) {
21        nsView.onClick = onClick
22    }
23}
24
25final class ClickMonitorNSView: NSView {
26    var onClick: (() -> Void)?
27    private var monitor: Any?
28
29    override func viewDidMoveToWindow() {
30        super.viewDidMoveToWindow()
31        if window != nil {
32            installMonitor()
33        } else {
34            removeMonitor()
35        }
36    }
37
38    private func installMonitor() {
39        guard monitor == nil else { return }
40        monitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) {
41            [weak self] event in
42            self?.handleClick(event)
43            return event
44        }
45    }
46
47    private func removeMonitor() {
48        if let monitor {
49            NSEvent.removeMonitor(monitor)
50            self.monitor = nil
51        }
52    }
53
54    private func handleClick(_ event: NSEvent) {
55        guard let window, event.window == window else { return }
56
57        let pointInSelf = convert(event.locationInWindow, from: nil)
58        guard bounds.contains(pointInSelf) else { return }
59
60        guard let hitView = window.contentView?.hitTest(event.locationInWindow)
61        else { return }
62
63        if hitView is NSClipView {
64            onClick?()
65        }
66    }
67
68}