macos: use macOS 15 pointerVisibility to show/hide cursor

This commit is contained in:
Mitchell Hashimoto
2024-09-20 21:21:05 -07:00
parent c6bbdfb7bf
commit e89a4f7408
4 changed files with 59 additions and 126 deletions

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>

View File

@ -82,6 +82,7 @@ extension Ghostty {
.focusedValue(\.ghosttySurfaceView, surfaceView) .focusedValue(\.ghosttySurfaceView, surfaceView)
.focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize) .focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize)
#if canImport(AppKit) #if canImport(AppKit)
.backport.pointerVisibility(surfaceView.pointerVisible ? .visible : .hidden)
.onReceive(pubBecomeKey) { notification in .onReceive(pubBecomeKey) { notification in
guard let window = notification.object as? NSWindow else { return } guard let window = notification.object as? NSWindow else { return }
guard let surfaceWindow = surfaceView.window else { return } guard let surfaceWindow = surfaceView.window else { return }

View File

@ -38,6 +38,9 @@ extension Ghostty {
// structure because I'm lazy. // structure because I'm lazy.
@Published var surfaceSize: ghostty_surface_size_s? = nil @Published var surfaceSize: ghostty_surface_size_s? = nil
// Whether the pointer should be visible or not
@Published var pointerVisible: Bool = true
// An initial size to request for a window. This will only affect // An initial size to request for a window. This will only affect
// then the view is moved to a new window. // then the view is moved to a new window.
var initialSize: NSSize? = nil var initialSize: NSSize? = nil
@ -97,11 +100,9 @@ extension Ghostty {
private(set) var surface: ghostty_surface_t? private(set) var surface: ghostty_surface_t?
private var markedText: NSMutableAttributedString private var markedText: NSMutableAttributedString
private var mouseEntered: Bool = false
private(set) var focused: Bool = true private(set) var focused: Bool = true
private var prevPressureStage: Int = 0 private var prevPressureStage: Int = 0
private var cursor: NSCursor = .iBeam private var cursor: NSCursor = .iBeam
private var cursorVisible: CursorVisibility = .visible
private var appearanceObserver: NSKeyValueObservation? = nil private var appearanceObserver: NSKeyValueObservation? = nil
// This is set to non-null during keyDown to accumulate insertText contents // This is set to non-null during keyDown to accumulate insertText contents
@ -114,15 +115,6 @@ extension Ghostty {
// so we'll use that to tell ghostty to refresh. // so we'll use that to tell ghostty to refresh.
override var wantsUpdateLayer: Bool { return true } override var wantsUpdateLayer: Bool { return true }
// State machine for mouse cursor visibility because every call to
// NSCursor.hide/unhide must be balanced.
enum CursorVisibility {
case visible
case hidden
case pendingVisible
case pendingHidden
}
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) { init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
self.markedText = NSMutableAttributedString() self.markedText = NSMutableAttributedString()
self.uuid = uuid ?? .init() self.uuid = uuid ?? .init()
@ -194,13 +186,6 @@ extension Ghostty {
trackingAreas.forEach { removeTrackingArea($0) } trackingAreas.forEach { removeTrackingArea($0) }
// mouseExited is not called by AppKit one last time when the view
// closes so we do it manually to ensure our NSCursor state remains
// accurate.
if (mouseEntered) {
mouseExited(with: NSEvent())
}
// Remove ourselves from secure input if we have to // Remove ourselves from secure input if we have to
SecureInput.shared.removeScoped(ObjectIdentifier(self)) SecureInput.shared.removeScoped(ObjectIdentifier(self))
@ -242,8 +227,6 @@ extension Ghostty {
} }
func sizeDidChange(_ size: CGSize) { func sizeDidChange(_ size: CGSize) {
guard let surface = self.surface else { return }
// Ghostty wants to know the actual framebuffer size... It is very important // Ghostty wants to know the actual framebuffer size... It is very important
// here that we use "size" and NOT the view frame. If we're in the middle of // here that we use "size" and NOT the view frame. If we're in the middle of
// an animation (i.e. a fullscreen animation), the frame will not yet be updated. // an animation (i.e. a fullscreen animation), the frame will not yet be updated.
@ -332,44 +315,10 @@ extension Ghostty {
// We ignore unknown shapes. // We ignore unknown shapes.
return return
} }
// Set our cursor immediately if our mouse is over our window
if (mouseEntered) { cursorUpdate(with: NSEvent()) }
if let window = self.window {
window.invalidateCursorRects(for: self)
}
} }
func setCursorVisibility(_ visible: Bool) { func setCursorVisibility(_ visible: Bool) {
switch (cursorVisible) { pointerVisible = visible
case .visible:
// If we want to be visible, do nothing. If we want to be hidden
// enter the pending state.
if (visible) { return }
cursorVisible = .pendingHidden
case .hidden:
// If we want to be hidden, do nothing. If we want to be visible
// enter the pending state.
if (!visible) { return }
cursorVisible = .pendingVisible
case .pendingVisible:
// If we want to be visible, do nothing because we're already pending.
// If we want to be hidden, we're already hidden so reset state.
if (visible) { return }
cursorVisible = .hidden
case .pendingHidden:
// If we want to be hidden, do nothing because we're pending that switch.
// If we want to be visible, we're already visible so reset state.
if (!visible) { return }
cursorVisible = .visible
}
if (mouseEntered) {
cursorUpdate(with: NSEvent())
}
} }
// MARK: - Notifications // MARK: - Notifications
@ -419,7 +368,6 @@ extension Ghostty {
addTrackingArea(NSTrackingArea( addTrackingArea(NSTrackingArea(
rect: frame, rect: frame,
options: [ options: [
.mouseEnteredAndExited,
.mouseMoved, .mouseMoved,
// Only send mouse events that happen in our visible (not obscured) rect // Only send mouse events that happen in our visible (not obscured) rect
@ -433,11 +381,6 @@ extension Ghostty {
userInfo: nil)) userInfo: nil))
} }
override func resetCursorRects() {
discardCursorRects()
addCursorRect(frame, cursor: self.cursor)
}
override func viewDidChangeBackingProperties() { override func viewDidChangeBackingProperties() {
super.viewDidChangeBackingProperties() super.viewDidChangeBackingProperties()
@ -578,40 +521,6 @@ extension Ghostty {
self.mouseMoved(with: event) self.mouseMoved(with: event)
} }
override func mouseEntered(with event: NSEvent) {
// For reasons unknown (Cocoaaaaaaaaa), mouseEntered is called
// multiple times in an unbalanced way with mouseExited when a new
// tab is created. In this scenario, we only want to process our
// callback once since this is stateful and we expect balancing.
if (mouseEntered) { return }
mouseEntered = true
// Update our cursor when we enter so we fully process our
// cursorVisible state.
cursorUpdate(with: NSEvent())
}
override func mouseExited(with event: NSEvent) {
// See mouseEntered
if (!mouseEntered) { return }
mouseEntered = false
// If the mouse is currently hidden, we want to show it when we exit
// this view. We go through the cursorVisible dance so that only
// cursorUpdate manages cursor state.
if (cursorVisible == .hidden) {
cursorVisible = .pendingVisible
cursorUpdate(with: NSEvent())
assert(cursorVisible == .visible)
// We set the state to pending hidden again for the next time
// we enter.
cursorVisible = .pendingHidden
}
}
override func scrollWheel(with event: NSEvent) { override func scrollWheel(with event: NSEvent) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
@ -675,24 +584,6 @@ extension Ghostty {
quickLook(with: event) quickLook(with: event)
} }
override func cursorUpdate(with event: NSEvent) {
switch (cursorVisible) {
case .visible, .hidden:
// Do nothing, stable state
break
case .pendingHidden:
NSCursor.hide()
cursorVisible = .hidden
case .pendingVisible:
NSCursor.unhide()
cursorVisible = .visible
}
cursor.set()
}
override func keyDown(with event: NSEvent) { override func keyDown(with event: NSEvent) {
guard let surface = self.surface else { guard let surface = self.surface else {
self.interpretKeyEvents([event]) self.interpretKeyEvents([event])

View File

@ -25,6 +25,14 @@ extension Backport where Content: Scene {
} }
extension Backport where Content: View { extension Backport where Content: View {
func pointerVisibility(_ v: BackportVisibility) -> some View {
if #available(macOS 15, *) {
return content.pointerVisibility(v.official)
} else {
return content
}
}
func pointerStyle(_ style: BackportPointerStyle) -> some View { func pointerStyle(_ style: BackportPointerStyle) -> some View {
if #available(macOS 15, *) { if #available(macOS 15, *) {
return content.pointerStyle(style.official) return content.pointerStyle(style.official)
@ -32,19 +40,52 @@ extension Backport where Content: View {
return content return content
} }
} }
}
enum BackportPointerStyle { enum BackportVisibility {
case grabIdle case automatic
case grabActive case visible
case link case hidden
@available(macOS 15, *) @available(macOS 15, *)
var official: PointerStyle { var official: Visibility {
switch self { switch self {
case .grabIdle: return .grabIdle case .automatic: return .automatic
case .grabActive: return .grabActive case .visible: return .visible
case .link: return .link case .hidden: return .hidden
} }
}
}
enum BackportPointerStyle {
case `default`
case grabIdle
case grabActive
case horizontalText
case verticalText
case link
case resizeLeft
case resizeRight
case resizeUp
case resizeDown
case resizeUpDown
case resizeLeftRight
@available(macOS 15, *)
var official: PointerStyle {
switch self {
case .default: return .default
case .grabIdle: return .grabIdle
case .grabActive: return .grabActive
case .horizontalText: return .horizontalText
case .verticalText: return .verticalText
case .link: return .link
case .resizeLeft: return .frameResize(position: .trailing, directions: [.inward])
case .resizeRight: return .frameResize(position: .leading, directions: [.inward])
case .resizeUp: return .frameResize(position: .bottom, directions: [.inward])
case .resizeDown: return .frameResize(position: .top, directions: [.inward])
case .resizeUpDown: return .frameResize(position: .top)
case .resizeLeftRight: return .frameResize(position: .trailing)
} }
} }
} }