From 7d2412c053f284b95befeb67a2a9f5f7e755bff4 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:26:44 -0600 Subject: [PATCH] General Performance Improvements (#24) --- .../CodeEditTextView/Cursors/CursorView.swift | 2 ++ .../TextLayoutManager/TextLayoutManager.swift | 24 +++++++++++++++---- .../TextLine/LineFragmentView.swift | 2 ++ .../CodeEditTextView/TextView/TextView.swift | 10 +++----- .../Utils/ViewReuseQueue.swift | 2 +- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Sources/CodeEditTextView/Cursors/CursorView.swift b/Sources/CodeEditTextView/Cursors/CursorView.swift index 105209a7..166e9ded 100644 --- a/Sources/CodeEditTextView/Cursors/CursorView.swift +++ b/Sources/CodeEditTextView/Cursors/CursorView.swift @@ -25,6 +25,8 @@ open class CursorView: NSView { true } + override open func hitTest(_ point: NSPoint) -> NSView? { nil } + /// Create a cursor view. /// - Parameters: /// - blinkDuration: The duration to blink, leave as nil to never blink. diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift index 0cc69376..11ce033d 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift @@ -54,6 +54,17 @@ public class TextLayoutManager: NSObject { } } + /// The amount of extra vertical padding used to lay out lines in before they come into view. + /// + /// This solves a small problem with layout performance, if you're seeing layout lagging behind while scrolling, + /// adjusting this value higher may help fix that. + /// Defaults to `350`. + public var verticalLayoutPadding: CGFloat = 350 { + didSet { + setNeedsLayout() + } + } + // MARK: - Internal weak var textStorage: NSTextStorage? @@ -215,10 +226,12 @@ public class TextLayoutManager: NSObject { } /// Ends a transaction. When called, the layout manager will layout any necessary lines. - public func endTransaction() { + public func endTransaction(forceLayout: Bool = false) { transactionCounter -= 1 if transactionCounter == 0 { - setNeedsLayout() + if forceLayout { + setNeedsLayout() + } layoutLines() } else if transactionCounter < 0 { // swiftlint:disable:next line_length @@ -237,8 +250,8 @@ public class TextLayoutManager: NSObject { return } CATransaction.begin() - let minY = max(visibleRect.minY, 0) - let maxY = max(visibleRect.maxY, 0) + let minY = max(visibleRect.minY - verticalLayoutPadding, 0) + let maxY = max(visibleRect.maxY + verticalLayoutPadding, 0) let originalHeight = lineStorage.height var usedFragmentIDs = Set() var forceLayout: Bool = needsLayout @@ -283,6 +296,8 @@ public class TextLayoutManager: NSObject { newVisibleLines.insert(linePosition.data.id) } + CATransaction.commit() + // Enqueue any lines not used in this layout pass. viewReuseQueue.enqueueViews(notInSet: usedFragmentIDs) @@ -302,7 +317,6 @@ public class TextLayoutManager: NSObject { } needsLayout = false - CATransaction.commit() } /// Lays out a single text line. diff --git a/Sources/CodeEditTextView/TextLine/LineFragmentView.swift b/Sources/CodeEditTextView/TextLine/LineFragmentView.swift index 485b303d..043c1829 100644 --- a/Sources/CodeEditTextView/TextLine/LineFragmentView.swift +++ b/Sources/CodeEditTextView/TextLine/LineFragmentView.swift @@ -20,6 +20,8 @@ final class LineFragmentView: NSView { false } + override func hitTest(_ point: NSPoint) -> NSView? { nil } + /// Prepare the view for reuse, clears the line fragment reference. override func prepareForReuse() { super.prepareForReuse() diff --git a/Sources/CodeEditTextView/TextView/TextView.swift b/Sources/CodeEditTextView/TextView/TextView.swift index dc34b96c..e7a4a6a7 100644 --- a/Sources/CodeEditTextView/TextView/TextView.swift +++ b/Sources/CodeEditTextView/TextView/TextView.swift @@ -381,15 +381,11 @@ public class TextView: NSView, NSTextContent { /// - Parameter point: The point to find. /// - Returns: A view at the given point, if any. override public func hitTest(_ point: NSPoint) -> NSView? { - // For our purposes, cursor and line fragment views should be transparent from the point of view of - // all other views. So, if the normal hitTest returns one of them, we return `self` instead. - let hitView = super.hitTest(point) - - if let hitView, hitView != self, - type(of: hitView) == CursorView.self || type(of: hitView) == LineFragmentView.self { + if visibleRect.contains(point) { return self + } else { + return super.hitTest(point) } - return hitView } // MARK: - Key Down diff --git a/Sources/CodeEditTextView/Utils/ViewReuseQueue.swift b/Sources/CodeEditTextView/Utils/ViewReuseQueue.swift index 6e44bf30..c969c573 100644 --- a/Sources/CodeEditTextView/Utils/ViewReuseQueue.swift +++ b/Sources/CodeEditTextView/Utils/ViewReuseQueue.swift @@ -52,7 +52,7 @@ public class ViewReuseQueue { /// Enqueues all views not in the given set. /// - Parameter outsideSet: The keys who's views should not be enqueued for reuse. public func enqueueViews(notInSet keys: Set) { - // Get all keys that are in "use" but not in the given set. + // Get all keys that are currently in "use" but not in the given set, and enqueue them for reuse. for key in Set(usedViews.keys).subtracting(keys) { enqueueView(forKey: key) }