mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: use macOS 15 pointerVisibility to show/hide cursor
This commit is contained in:
@ -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>
|
||||||
|
@ -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 }
|
||||||
|
@ -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])
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user