Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optionally Use System Cursor #21

Merged
merged 4 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Sources/CodeEditTextView/Extensions/GC+ApproximateEqual.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// GC+ApproximateEqual.swift
// CodeEditTextView
//
// Created by Khan Winter on 2/16/24.
//

import Foundation

extension CGFloat {
func approxEqual(_ other: CGFloat, tolerance: CGFloat = 0.5) -> Bool {
abs(self - other) <= tolerance
}
}

extension CGPoint {
func approxEqual(_ other: CGPoint, tolerance: CGFloat = 0.5) -> Bool {
return self.x.approxEqual(other.x, tolerance: tolerance)
&& self.y.approxEqual(other.y, tolerance: tolerance)
}
}

extension CGRect {
func approxEqual(_ other: CGRect, tolerance: CGFloat = 0.5) -> Bool {
return self.origin.approxEqual(other.origin, tolerance: tolerance)
&& self.width.approxEqual(other.width, tolerance: tolerance)
&& self.height.approxEqual(other.height, tolerance: tolerance)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class TextSelectionManager: NSObject {

public class TextSelection: Hashable, Equatable {
public var range: NSRange
weak var view: CursorView?
weak var view: NSView?
var boundingRect: CGRect = .zero
var suggestedXPos: CGFloat?
/// The position this selection should 'rotate' around when modifying selections.
Expand Down Expand Up @@ -71,12 +71,17 @@ public class TextSelectionManager: NSObject {

public var insertionPointColor: NSColor = NSColor.labelColor {
didSet {
textSelections.forEach { $0.view?.color = insertionPointColor }
textSelections.compactMap({ $0.view as? CursorView }).forEach { $0.color = insertionPointColor }
}
}
public var highlightSelectedLine: Bool = true
public var selectedLineBackgroundColor: NSColor = NSColor.selectedTextBackgroundColor.withSystemEffect(.disabled)
public var selectionBackgroundColor: NSColor = NSColor.selectedTextBackgroundColor
public var useSystemCursor: Bool = false {
didSet {
updateSelectionViews()
}
}

internal(set) public var textSelections: [TextSelection] = []
weak var layoutManager: TextLayoutManager?
Expand All @@ -89,7 +94,8 @@ public class TextSelectionManager: NSObject {
layoutManager: TextLayoutManager,
textStorage: NSTextStorage,
textView: TextView?,
delegate: TextSelectionManagerDelegate?
delegate: TextSelectionManagerDelegate?,
useSystemCursor: Bool = false
) {
self.layoutManager = layoutManager
self.textStorage = textStorage
Expand Down Expand Up @@ -168,21 +174,47 @@ public class TextSelectionManager: NSObject {
for textSelection in textSelections {
if textSelection.range.isEmpty {
let cursorOrigin = (layoutManager?.rectForOffset(textSelection.range.location) ?? .zero).origin
if textSelection.view == nil
|| textSelection.boundingRect.origin != cursorOrigin
|| textSelection.boundingRect.height != layoutManager?.estimateLineHeight() ?? 0 {
textSelection.view?.removeFromSuperview()
textSelection.view = nil

let cursorView = CursorView(color: insertionPointColor)
var doesViewNeedReposition: Bool

// If using the system cursor, macOS will change the origin and height by about 0.5, so we do an
// approximate equals in that case to avoid extra updates.
if useSystemCursor, #available(macOS 14.0, *) {
doesViewNeedReposition = !textSelection.boundingRect.origin.approxEqual(cursorOrigin)
|| !textSelection.boundingRect.height.approxEqual(layoutManager?.estimateLineHeight() ?? 0)
} else {
doesViewNeedReposition = textSelection.boundingRect.origin != cursorOrigin
|| textSelection.boundingRect.height != layoutManager?.estimateLineHeight() ?? 0
}

if textSelection.view == nil || doesViewNeedReposition {
let cursorView: NSView

if let existingCursorView = textSelection.view {
cursorView = existingCursorView
} else {
textSelection.view?.removeFromSuperview()
textSelection.view = nil

if useSystemCursor, #available(macOS 14.0, *) {
let systemCursorView = NSTextInsertionIndicator(frame: .zero)
cursorView = systemCursorView
systemCursorView.displayMode = .automatic
} else {
let internalCursorView = CursorView(color: insertionPointColor)
cursorView = internalCursorView
cursorTimer.register(internalCursorView)
}

textView?.addSubview(cursorView)
}

cursorView.frame.origin = cursorOrigin
cursorView.frame.size.height = layoutManager?.estimateLineHeight() ?? 0
textView?.addSubview(cursorView)

textSelection.view = cursorView
textSelection.boundingRect = cursorView.frame

cursorTimer.register(cursorView)

didUpdate = true
}
} else if !textSelection.range.isEmpty && textSelection.view != nil {
Expand Down
31 changes: 31 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+Setup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,35 @@ extension TextView {
delegate: self
)
}

func setUpScrollListeners(scrollView: NSScrollView) {
NotificationCenter.default.removeObserver(self, name: NSScrollView.willStartLiveScrollNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: NSScrollView.didEndLiveScrollNotification, object: nil)

NotificationCenter.default.addObserver(
self,
selector: #selector(scrollViewWillStartScroll),
name: NSScrollView.willStartLiveScrollNotification,
object: scrollView
)

NotificationCenter.default.addObserver(
self,
selector: #selector(scrollViewDidEndScroll),
name: NSScrollView.didEndLiveScrollNotification,
object: scrollView
)
}

@objc func scrollViewWillStartScroll() {
if #available(macOS 14.0, *) {
inputContext?.textInputClientWillStartScrollingOrZooming()
}
}

@objc func scrollViewDidEndScroll() {
if #available(macOS 14.0, *) {
inputContext?.textInputClientDidEndScrollingOrZooming()
}
}
}
27 changes: 27 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,22 @@ public class TextView: NSView, NSTextContent {
}
}

/// Determines if the text view uses the macOS system cursor or a ``CursorView`` for cursors.
///
/// - Important: Only available after macOS 14.
public var useSystemCursor: Bool {
get {
selectionManager?.useSystemCursor ?? false
}
set {
guard #available(macOS 14, *) else {
logger.warning("useSystemCursor only available after macOS 14.")
return
}
selectionManager?.useSystemCursor = newValue
}
}

open var contentType: NSTextContentType?

/// The text view's delegate.
Expand Down Expand Up @@ -225,6 +241,7 @@ public class TextView: NSView, NSTextContent {
/// - isEditable: Determines if the view is editable.
/// - isSelectable: Determines if the view is selectable.
/// - letterSpacing: Sets the letter spacing on the view.
/// - useSystemCursor: Set to true to use the system cursor. Only available in macOS >= 14.
/// - delegate: The text view's delegate.
public init(
string: String,
Expand All @@ -235,6 +252,7 @@ public class TextView: NSView, NSTextContent {
isEditable: Bool,
isSelectable: Bool,
letterSpacing: Double,
useSystemCursor: Bool = false,
delegate: TextViewDelegate
) {
self.textStorage = NSTextStorage(string: string)
Expand Down Expand Up @@ -264,6 +282,7 @@ public class TextView: NSView, NSTextContent {
layoutManager = setUpLayoutManager(lineHeightMultiplier: lineHeightMultiplier, wrapLines: wrapLines)
storageDelegate.addDelegate(layoutManager)
selectionManager = setUpSelectionManager()
selectionManager.useSystemCursor = useSystemCursor

_undoManager = CEUndoManager(textView: self)

Expand Down Expand Up @@ -370,6 +389,14 @@ public class TextView: NSView, NSTextContent {
layoutManager.layoutLines()
}

override public func viewWillMove(toSuperview newSuperview: NSView?) {
guard let scrollView = enclosingScrollView else {
return
}

setUpScrollListeners(scrollView: scrollView)
}

override public func viewDidEndLiveResize() {
super.viewDidEndLiveResize()
updateFrameIfNeeded()
Expand Down
Loading