Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI System Cursor

I'm trying to change the cursor to a crosshair in SwiftUI on MacOS.

I've put the following code into the AppDelegate applicationDidFinishLaunching() function:

NSCursor.crosshair.set()

When the application loads, I see the cursor change to the crossHair, and then swap straight back to the standard pointer.

It would be great to hear what I'm doing wrong here.

Thanks all.

like image 220
Jacob Shooter Avatar asked May 24 '20 10:05

Jacob Shooter


2 Answers

It is not the way how NSCursor works. There is a stack of cursors so any standard (or non-standard) control can push own type of cursor on top and make that cursor current. So you just place first cursor, but then some standard view having own default cursor replaces it.

As SwiftUI does not allow now to manage cursors natively, the solution might be

a) either to set/push desired cursor on SwiftUI view appear/disappear, or

b) add cursor rect to NSHostingController view using standard NSView.addCursorRect method.

Update: some quick demo from existing project (part of custom button solution)

demo

struct DemoCustomCursor: View {
    var body: some View {
        Button(action: {}) {
            Text("Cross Button")
                .padding(20)
                .background(Color.blue)
        }.buttonStyle(PlainButtonStyle())
        .onHover { inside in
            if inside {
                NSCursor.crosshair.push()
            } else {
                NSCursor.pop()
            }
        }
    }
}

backup

like image 194
Asperi Avatar answered Sep 20 '22 19:09

Asperi


Here is Swift Playground code to illustrate the problem:

import Foundation
import SwiftUI
import PlaygroundSupport

PlaygroundPage.current.setLiveView(
    SplitView().frame(width: 600, height:600).border(Color.black)
)

struct SplitView: View {
    @State var position: CGFloat = 10.0
    var body: some View {
        Text("top")
            .frame(height: position)
        PaneDivider(position: $position)
        Text("bottom")
        Spacer()
    }
}

struct PaneDivider: View {
    @Binding var position: CGFloat
    @GestureState private var isDragging = false // Will reset to false when dragging has ended
       
    var body: some View {
        Rectangle().frame(height:10).foregroundColor(Color.gray)
        .onHover { inside in
            if !isDragging {
                if inside { NSCursor.resizeUpDown.push() }
                else {  NSCursor.pop() }
                }
        }
        .gesture(DragGesture()
            .onChanged { position += $0.translation.height  }
            .updating($isDragging) { (value, state, transaction) in
                if !state { NSCursor.resizeUpDown.push() } // This is overridden, something else in the system is pushing the arrow cursor during the drag
                state = true
            }
            .onEnded { _ in NSCursor.pop() })
    }
}

Setting a breakpoint on -[NSCursor set] shows it firing constantly during the drag in

Setting a breakpoint on -[NSCursor set] shows it firing constantly during the drag in
  * frame #0: 0x00000001a48c2944 AppKit`-[NSCursor set]
    frame #1: 0x00000001a491cc4c AppKit`forwardMethod + 200
    frame #2: 0x00000001a491cc4c AppKit`forwardMethod + 200
    frame #3: 0x00000001a4924428 AppKit`-[NSView cursorUpdate:] + 132
    frame #4: 0x00000001a48cb2e4 AppKit`-[NSWindow(NSCursorRects) _setCursorForMouseLocation:] + 432
    frame #5: 0x00000001a47f0d70 AppKit`_NSWindowDisplayCycleUpdateStructuralRegions + 544
    frame #6: 0x00000001a47ec028 AppKit`__NSWindowGetDisplayCycleObserverForUpdateStructuralRegions_block_invoke + 428
    frame #7: 0x00000001a47e59ec AppKit`NSDisplayCycleObserverInvoke + 188
    frame #8: 0x00000001a47e5568 AppKit`NSDisplayCycleFlush + 832
    frame #9: 0x00000001a81a8544 QuartzCore`CA::Transaction::run_commit_handlers(CATransactionPhase) + 120
    frame #10: 0x00000001a81a754c QuartzCore`CA::Transaction::commit() + 336
    frame #11: 0x00000001a488e6e0 AppKit`__62+[CATransaction(NSCATransaction) NS_setFlushesWithDisplayLink]_block_invoke + 304
    frame #12: 0x00000001a4fe2a10 AppKit`___NSRunLoopObserverCreateWithHandler_block_invoke + 64
    frame #13: 0x00000001a1f28e14 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 36
    frame #14: 0x00000001a1f28c60 CoreFoundation`__CFRunLoopDoObservers + 572
    frame #15: 0x00000001a1f281a8 CoreFoundation`__CFRunLoopRun + 764
    frame #16: 0x00000001a1f27734 CoreFoundation`CFRunLoopRunSpecific + 600
    frame #17: 0x00000001a9e25b84 HIToolbox`RunCurrentEventLoopInMode + 292
    frame #18: 0x00000001a9e258f8 HIToolbox`ReceiveNextEventCommon + 552
    frame #19: 0x00000001a9e256b8 HIToolbox`_BlockUntilNextEventMatchingListInModeWithFilter + 72
    frame #20: 0x00000001a47114ec AppKit`_DPSNextEvent + 836
    frame #21: 0x00000001a470fe8c AppKit`-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 1292
    frame #22: 0x00000001a4701d18 AppKit`-[NSApplication run] + 596
    frame #23: 0x00000001a46d3728 AppKit`NSApplicationMain + 1064
    frame #24: 0x00000001c25f11b4 SwiftUI`generic specialization <SwiftUI.TestingAppDelegate> of function signature specialization <Arg[0] = Existential To Protocol Constrained Generic> of SwiftUI.runApp(__C.NSApplicationDelegate) -> Swift.Never + 96
    frame #25: 0x00000001c2e34ba0 SwiftUI`SwiftUI.runApp<τ_0_0 where τ_0_0: SwiftUI.App>(τ_0_0) -> Swift.Never + 220
    frame #26: 0x00000001c29de854 SwiftUI`static SwiftUI.App.main() -> () + 128
    frame #27: 0x0000000100c8c540 Calculator`static CalculatorApp.$main(self=Calculator.CalculatorApp) at CalculatorApp.swift:5:1
    frame #28: 0x0000000100c8c5e0 Calculator`main at CalculatorApp.swift:0
    frame #29: 0x00000001a1e48420 libdyld.dylib`start + 4

Looks like the same pre-SwiftUI problem as http://cocoadev.github.io/CursorFlashingProblems/

Adding NSApp.windows[0].disableCursorRects() at the start of the DragGesture and NSApp.windows[0].enableCursorRects() in .onEnded fixes the problem for me here.

Except NSApp.windows[0] isn't always the front window. Darn. NSApp.windows.forEach { $0.disableCursorRects() } works though. ;)

like image 37
Avitzur Avatar answered Sep 20 '22 19:09

Avitzur