mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
Merge pull request #2281 from ghostty-org/pointer
macOS: Use macOS 15 APIs to stabilize cursor hide while typing
This commit is contained in:
@ -59,6 +59,8 @@
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
|
||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
|
||||
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */; };
|
||||
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36142C9CDA03004D6760 /* View+Extension.swift */; };
|
||||
A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; };
|
||||
@ -128,6 +130,7 @@
|
||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = "<group>"; };
|
||||
A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInputOverlay.swift; sourceTree = "<group>"; };
|
||||
A5CC36142C9CDA03004D6760 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = "<group>"; };
|
||||
A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = "<group>"; };
|
||||
@ -212,6 +215,7 @@
|
||||
children = (
|
||||
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
||||
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
|
||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
||||
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */,
|
||||
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
||||
@ -527,6 +531,7 @@
|
||||
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */,
|
||||
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */,
|
||||
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
|
||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */,
|
||||
A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */,
|
||||
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
|
||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
||||
@ -562,6 +567,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */,
|
||||
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */,
|
||||
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */,
|
||||
|
@ -34,6 +34,9 @@ struct ClipboardConfirmationView: View {
|
||||
/// Optional delegate to get results. If this is nil, then this view will never close on its own.
|
||||
weak var delegate: ClipboardConfirmationViewDelegate? = nil
|
||||
|
||||
/// Used to track if we should rehide on disappear
|
||||
@State private var cursorHiddenCount: UInt = 0
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
@ -65,6 +68,25 @@ struct ClipboardConfirmationView: View {
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
.onAppear {
|
||||
// I can't find a better way to handle this. There is no API to detect
|
||||
// if the cursor is hidden and OTHER THINGS do unhide the cursor. So we
|
||||
// try to unhide it completely here and hope for the best. Issue #1516.
|
||||
cursorHiddenCount = Cursor.unhideCompletely()
|
||||
|
||||
// If we didn't unhide anything, we just send an unhide to be safe.
|
||||
// I don't think the count can go negative on NSCursor so this handles
|
||||
// scenarios cursor is hidden outside of our own NSCursor usage.
|
||||
if (cursorHiddenCount == 0) {
|
||||
_ = Cursor.unhide()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
// Rehide if we unhid
|
||||
for _ in 0..<cursorHiddenCount {
|
||||
Cursor.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func onCancel() {
|
||||
|
@ -1,8 +1,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>
|
||||
<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"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
|
@ -59,6 +59,23 @@ extension Ghostty {
|
||||
|
||||
@EnvironmentObject private var ghostty: Ghostty.App
|
||||
|
||||
#if canImport(AppKit)
|
||||
// The visibility state of the mouse pointer
|
||||
private var pointerVisibility: BackportVisibility {
|
||||
// If our window or surface loses focus we always bring it back
|
||||
if (!windowFocus || !surfaceFocus) {
|
||||
return .visible
|
||||
}
|
||||
|
||||
// If we have window focus then it depends on surface state
|
||||
if (surfaceView.pointerVisible) {
|
||||
return .visible
|
||||
} else {
|
||||
return .hidden
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
let center = NotificationCenter.default
|
||||
|
||||
@ -82,6 +99,8 @@ extension Ghostty {
|
||||
.focusedValue(\.ghosttySurfaceView, surfaceView)
|
||||
.focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize)
|
||||
#if canImport(AppKit)
|
||||
.backport.pointerVisibility(pointerVisibility)
|
||||
.backport.pointerStyle(surfaceView.pointerStyle)
|
||||
.onReceive(pubBecomeKey) { notification in
|
||||
guard let window = notification.object as? NSWindow else { return }
|
||||
guard let surfaceWindow = surfaceView.window else { return }
|
||||
|
@ -38,6 +38,10 @@ extension Ghostty {
|
||||
// structure because I'm lazy.
|
||||
@Published var surfaceSize: ghostty_surface_size_s? = nil
|
||||
|
||||
// Whether the pointer should be visible or not
|
||||
@Published private(set) var pointerVisible: Bool = true
|
||||
@Published private(set) var pointerStyle: BackportPointerStyle = .default
|
||||
|
||||
// An initial size to request for a window. This will only affect
|
||||
// then the view is moved to a new window.
|
||||
var initialSize: NSSize? = nil
|
||||
@ -97,11 +101,8 @@ extension Ghostty {
|
||||
|
||||
private(set) var surface: ghostty_surface_t?
|
||||
private var markedText: NSMutableAttributedString
|
||||
private var mouseEntered: Bool = false
|
||||
private(set) var focused: Bool = true
|
||||
private var prevPressureStage: Int = 0
|
||||
private var cursor: NSCursor = .iBeam
|
||||
private var cursorVisible: CursorVisibility = .visible
|
||||
private var appearanceObserver: NSKeyValueObservation? = nil
|
||||
|
||||
// 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.
|
||||
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) {
|
||||
self.markedText = NSMutableAttributedString()
|
||||
self.uuid = uuid ?? .init()
|
||||
@ -194,13 +186,6 @@ extension Ghostty {
|
||||
|
||||
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
|
||||
SecureInput.shared.removeScoped(ObjectIdentifier(self))
|
||||
|
||||
@ -242,8 +227,6 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
func sizeDidChange(_ size: CGSize) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
// 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
|
||||
// an animation (i.e. a fullscreen animation), the frame will not yet be updated.
|
||||
@ -284,92 +267,58 @@ extension Ghostty {
|
||||
func setCursorShape(_ shape: ghostty_mouse_shape_e) {
|
||||
switch (shape) {
|
||||
case GHOSTTY_MOUSE_SHAPE_DEFAULT:
|
||||
cursor = .arrow
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU:
|
||||
cursor = .contextualMenu
|
||||
pointerStyle = .default
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_TEXT:
|
||||
cursor = .iBeam
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_CROSSHAIR:
|
||||
cursor = .crosshair
|
||||
pointerStyle = .horizontalText
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_GRAB:
|
||||
cursor = .openHand
|
||||
pointerStyle = .grabIdle
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_GRABBING:
|
||||
cursor = .closedHand
|
||||
pointerStyle = .grabActive
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_POINTER:
|
||||
cursor = .pointingHand
|
||||
pointerStyle = .link
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_W_RESIZE:
|
||||
cursor = .resizeLeft
|
||||
pointerStyle = .resizeLeft
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_E_RESIZE:
|
||||
cursor = .resizeRight
|
||||
pointerStyle = .resizeRight
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_N_RESIZE:
|
||||
cursor = .resizeUp
|
||||
pointerStyle = .resizeUp
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_S_RESIZE:
|
||||
cursor = .resizeDown
|
||||
pointerStyle = .resizeDown
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_NS_RESIZE:
|
||||
cursor = .resizeUpDown
|
||||
pointerStyle = .resizeUpDown
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_EW_RESIZE:
|
||||
cursor = .resizeLeftRight
|
||||
pointerStyle = .resizeLeftRight
|
||||
|
||||
case GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT:
|
||||
cursor = .iBeamCursorForVerticalLayout
|
||||
pointerStyle = .default
|
||||
|
||||
// These are not yet supported. We should support them by constructing a
|
||||
// PointerStyle from an NSCursor.
|
||||
case GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU:
|
||||
fallthrough
|
||||
case GHOSTTY_MOUSE_SHAPE_CROSSHAIR:
|
||||
fallthrough
|
||||
case GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED:
|
||||
cursor = .operationNotAllowed
|
||||
pointerStyle = .default
|
||||
|
||||
default:
|
||||
// We ignore unknown shapes.
|
||||
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) {
|
||||
switch (cursorVisible) {
|
||||
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())
|
||||
}
|
||||
pointerVisible = visible
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
@ -419,7 +368,6 @@ extension Ghostty {
|
||||
addTrackingArea(NSTrackingArea(
|
||||
rect: frame,
|
||||
options: [
|
||||
.mouseEnteredAndExited,
|
||||
.mouseMoved,
|
||||
|
||||
// Only send mouse events that happen in our visible (not obscured) rect
|
||||
@ -433,11 +381,6 @@ extension Ghostty {
|
||||
userInfo: nil))
|
||||
}
|
||||
|
||||
override func resetCursorRects() {
|
||||
discardCursorRects()
|
||||
addCursorRect(frame, cursor: self.cursor)
|
||||
}
|
||||
|
||||
override func viewDidChangeBackingProperties() {
|
||||
super.viewDidChangeBackingProperties()
|
||||
|
||||
@ -578,40 +521,6 @@ extension Ghostty {
|
||||
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) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
@ -675,24 +584,6 @@ extension Ghostty {
|
||||
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) {
|
||||
guard let surface = self.surface else {
|
||||
self.interpretKeyEvents([event])
|
||||
|
@ -25,26 +25,77 @@ extension Backport where Content: Scene {
|
||||
}
|
||||
|
||||
extension Backport where Content: View {
|
||||
func pointerStyle(_ style: BackportPointerStyle) -> some View {
|
||||
func pointerVisibility(_ v: BackportVisibility) -> some View {
|
||||
#if canImport(AppKit)
|
||||
if #available(macOS 15, *) {
|
||||
return content.pointerStyle(style.official)
|
||||
return content.pointerVisibility(v.official)
|
||||
} else {
|
||||
return content
|
||||
}
|
||||
#else
|
||||
return content
|
||||
#endif
|
||||
}
|
||||
|
||||
enum BackportPointerStyle {
|
||||
case grabIdle
|
||||
case grabActive
|
||||
case link
|
||||
func pointerStyle(_ style: BackportPointerStyle?) -> some View {
|
||||
#if canImport(AppKit)
|
||||
if #available(macOS 15, *) {
|
||||
return content.pointerStyle(style?.official)
|
||||
} else {
|
||||
return content
|
||||
}
|
||||
#else
|
||||
return content
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 15, *)
|
||||
var official: PointerStyle {
|
||||
switch self {
|
||||
case .grabIdle: return .grabIdle
|
||||
case .grabActive: return .grabActive
|
||||
case .link: return .link
|
||||
}
|
||||
enum BackportVisibility {
|
||||
case automatic
|
||||
case visible
|
||||
case hidden
|
||||
|
||||
@available(macOS 15, *)
|
||||
var official: Visibility {
|
||||
switch self {
|
||||
case .automatic: return .automatic
|
||||
case .visible: return .visible
|
||||
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
|
||||
|
||||
#if canImport(AppKit)
|
||||
@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)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
38
macos/Sources/Helpers/Cursor.swift
Normal file
38
macos/Sources/Helpers/Cursor.swift
Normal file
@ -0,0 +1,38 @@
|
||||
import Cocoa
|
||||
|
||||
/// This helps manage the stateful nature of NSCursor hiding and unhiding.
|
||||
class Cursor {
|
||||
private static var counter: UInt = 0
|
||||
|
||||
static var isVisible: Bool {
|
||||
counter == 0
|
||||
}
|
||||
|
||||
static func hide() {
|
||||
counter += 1
|
||||
NSCursor.hide()
|
||||
}
|
||||
|
||||
/// Unhide the cursor. Returns true if the cursor was previously hidden.
|
||||
static func unhide() -> Bool {
|
||||
// Its always safe to call unhide when the counter is zero because it
|
||||
// won't go negative.
|
||||
NSCursor.unhide()
|
||||
|
||||
if (counter > 0) {
|
||||
counter -= 1
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
static func unhideCompletely() -> UInt {
|
||||
let counter = self.counter
|
||||
for _ in 0..<counter {
|
||||
assert(unhide())
|
||||
}
|
||||
assert(self.counter == 0)
|
||||
return counter
|
||||
}
|
||||
}
|
@ -44,15 +44,30 @@ extension SplitView {
|
||||
}
|
||||
}
|
||||
|
||||
private var pointerStyle: BackportPointerStyle {
|
||||
return switch (direction) {
|
||||
case .horizontal: .resizeLeftRight
|
||||
case .vertical: .resizeUpDown
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.clear
|
||||
.frame(width: invisibleWidth, height: invisibleHeight)
|
||||
.contentShape(Rectangle()) // Makes it hit testable for pointerStyle
|
||||
Rectangle()
|
||||
.fill(color)
|
||||
.frame(width: visibleWidth, height: visibleHeight)
|
||||
}
|
||||
.backport.pointerStyle(pointerStyle)
|
||||
.onHover { isHovered in
|
||||
// macOS 15+ we use the pointerStyle helper which is much less
|
||||
// error-prone versus manual NSCursor push/pop
|
||||
if #available(macOS 15, *) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isHovered) {
|
||||
switch (direction) {
|
||||
case .horizontal:
|
||||
|
@ -16,3 +16,16 @@ extension View {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func pointerStyleFromCursor(_ cursor: NSCursor) -> some View {
|
||||
if #available(macOS 15.0, *) {
|
||||
return self.pointerStyle(.image(
|
||||
Image(nsImage: cursor.image),
|
||||
hotSpot: .init(x: cursor.hotSpot.x, y: cursor.hotSpot.y)
|
||||
))
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -429,6 +429,8 @@ palette: Palette = .{},
|
||||
/// Hide the mouse immediately when typing. The mouse becomes visible again when
|
||||
/// the mouse is used. The mouse is only hidden if the mouse cursor is over the
|
||||
/// active terminal surface.
|
||||
///
|
||||
/// macOS: This feature requires macOS 15.0 (Sequoia) or later.
|
||||
@"mouse-hide-while-typing": bool = false,
|
||||
|
||||
/// Determines whether running programs can detect the shift key pressed with a
|
||||
|
Reference in New Issue
Block a user