From 1c171b206223b30e5c004f5ed457b6c3f650ea10 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:54:18 -0600 Subject: [PATCH] Clean up implementation --- .../Extensions/GC+ApproximateEqual.swift | 29 ++++++++++++ .../TextSelectionManager.swift | 46 +++++++++++++------ .../TextView/TextView+Setup.swift | 31 +++++++++++++ .../CodeEditTextView/TextView/TextView.swift | 11 +++++ 4 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 Sources/CodeEditTextView/Extensions/GC+ApproximateEqual.swift diff --git a/Sources/CodeEditTextView/Extensions/GC+ApproximateEqual.swift b/Sources/CodeEditTextView/Extensions/GC+ApproximateEqual.swift new file mode 100644 index 00000000..ef0a258b --- /dev/null +++ b/Sources/CodeEditTextView/Extensions/GC+ApproximateEqual.swift @@ -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) + } +} diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift index 76678632..effcae62 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift @@ -94,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 @@ -173,29 +174,44 @@ 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 + 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 useSystemCursor, #available(macOS 14.0, *) { - let systemCursorView = NSTextInsertionIndicator(frame: .zero) - cursorView = systemCursorView - systemCursorView.displayMode = .visible + if let existingCursorView = textSelection.view { + cursorView = existingCursorView } else { - let internalCursorView = CursorView(color: insertionPointColor) - cursorView = internalCursorView - cursorTimer.register(internalCursorView) + 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 diff --git a/Sources/CodeEditTextView/TextView/TextView+Setup.swift b/Sources/CodeEditTextView/TextView/TextView+Setup.swift index e315a480..c894cf04 100644 --- a/Sources/CodeEditTextView/TextView/TextView+Setup.swift +++ b/Sources/CodeEditTextView/TextView/TextView+Setup.swift @@ -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() + } + } } diff --git a/Sources/CodeEditTextView/TextView/TextView.swift b/Sources/CodeEditTextView/TextView/TextView.swift index 54dc1295..735b71e0 100644 --- a/Sources/CodeEditTextView/TextView/TextView.swift +++ b/Sources/CodeEditTextView/TextView/TextView.swift @@ -241,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, @@ -251,6 +252,7 @@ public class TextView: NSView, NSTextContent { isEditable: Bool, isSelectable: Bool, letterSpacing: Double, + useSystemCursor: Bool = false, delegate: TextViewDelegate ) { self.textStorage = NSTextStorage(string: string) @@ -280,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) @@ -386,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()