Skip to content

Commit

Permalink
Optionally Use System Cursor (#21)
Browse files Browse the repository at this point in the history
### Description

Adds the ability to use the system cursor if available and enabled using a `useSystemCursor` variable on either `TextView` or `TextSelectionManager`.

### Related Issues

*  closes #5

### Checklist

<!--- Add things that are not yet implemented above -->

- [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md)
- [x] The issues this PR addresses are related to each other
- [x] My changes generate no new warnings
- [x] My code builds and runs on my machine
- [x] My changes are all related to the related issue above
- [x] I documented my code

### Screenshots

Example usage in CodeEditSourceEditor:

https://github.com/CodeEditApp/CodeEditTextView/assets/35942988/5b5adeee-dd80-4f92-92d0-121be18ab6ae
  • Loading branch information
thecoolwinter authored Feb 21, 2024
1 parent 7d2412c commit 6653c21
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 12 deletions.
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

0 comments on commit 6653c21

Please sign in to comment.