From f8aea7745b8dfabc978c7c2010ac573b1c7661a3 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 20 Feb 2024 18:38:19 -0600 Subject: [PATCH 1/3] Begin Search for Performance Improvements --- Sources/CodeEditTextView/Cursors/CursorView.swift | 2 ++ .../TextLayoutManager/TextLayoutManager.swift | 13 ++++++++++--- .../TextLine/LineFragmentView.swift | 2 ++ Sources/CodeEditTextView/TextView/TextView.swift | 10 +++------- Sources/CodeEditTextView/Utils/ViewReuseQueue.swift | 2 +- 5 files changed, 18 insertions(+), 11 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 400d013a..b9bebd0e 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift @@ -215,10 +215,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 @@ -241,6 +243,10 @@ public class TextLayoutManager: NSObject { var yContentAdjustment: CGFloat = 0 var maxFoundLineWidth = maxLineWidth + var info = mach_timebase_info() + guard mach_timebase_info(&info) == KERN_SUCCESS else { return } + let start = mach_absolute_time() + // Layout all lines for linePosition in lineStorage.linesStartingAt(minY, until: maxY) { // Updating height in the loop may cause the iterator to be wrong @@ -278,6 +284,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) @@ -297,7 +305,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 e971de57..0d1565d2 100644 --- a/Sources/CodeEditTextView/TextLine/LineFragmentView.swift +++ b/Sources/CodeEditTextView/TextLine/LineFragmentView.swift @@ -19,6 +19,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 90285fd3..5b6dee24 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) } From 31f8bda0266cc10c410603944d1d531c7d6ecd1d Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 20 Feb 2024 18:44:52 -0600 Subject: [PATCH 2/3] Add Padding to Layout Rect --- .../TextLayoutManager/TextLayoutManager.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift index b9bebd0e..74630a26 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift @@ -234,8 +234,8 @@ public class TextLayoutManager: NSObject { func layoutLines() { // swiftlint:disable:this function_body_length guard let visibleRect = delegate?.visibleRect, !isInTransaction, let textStorage else { return } CATransaction.begin() - let minY = max(visibleRect.minY, 0) - let maxY = max(visibleRect.maxY, 0) + let minY = max(visibleRect.minY - 350, 0) // +-350px for a bit padding while laying out lines. + let maxY = max(visibleRect.maxY + 350, 0) let originalHeight = lineStorage.height var usedFragmentIDs = Set() var forceLayout: Bool = needsLayout @@ -243,10 +243,6 @@ public class TextLayoutManager: NSObject { var yContentAdjustment: CGFloat = 0 var maxFoundLineWidth = maxLineWidth - var info = mach_timebase_info() - guard mach_timebase_info(&info) == KERN_SUCCESS else { return } - let start = mach_absolute_time() - // Layout all lines for linePosition in lineStorage.linesStartingAt(minY, until: maxY) { // Updating height in the loop may cause the iterator to be wrong From b98d99222ab3387992b7427c61a36ec542561d29 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 20 Feb 2024 19:15:43 -0600 Subject: [PATCH 3/3] Configurable Vertical Layout Padding --- .../TextLayoutManager/TextLayoutManager.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift index 74630a26..d88c6168 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? @@ -234,8 +245,8 @@ public class TextLayoutManager: NSObject { func layoutLines() { // swiftlint:disable:this function_body_length guard let visibleRect = delegate?.visibleRect, !isInTransaction, let textStorage else { return } CATransaction.begin() - let minY = max(visibleRect.minY - 350, 0) // +-350px for a bit padding while laying out lines. - let maxY = max(visibleRect.maxY + 350, 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