From 509d7b2e86460e8ec15b0dd5410cbc8e8c05940f Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 18 Nov 2024 09:13:05 -0600 Subject: [PATCH] Move All Delegate Calls Outside Layout Pass (#57) - Moves a few more delegate calls or delegate triggering calls outside the protected area in `layoutLines` to prevent crashes. These are safe to call outside the intended protected area. - Documents an important layout method. - Fixes a flaking test by sorting selection ranges after deleting. --- .../TextLayoutManager/TextLayoutManager.swift | 15 +++++++++------ .../TextView/TextView+Delete.swift | 1 + .../TextView/TextView+Layout.swift | 2 ++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift index 788c76b6..7e0e5606 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift @@ -266,6 +266,13 @@ public class TextLayoutManager: NSObject { // Update the visible lines with the new set. visibleLineIds = newVisibleLines + #if DEBUG + isInLayout = false + #endif + + // These are fine to update outside of `isInLayout` as our internal data structures are finalized at this point + // so laying out again won't break our line storage or visible line. + if maxFoundLineWidth > maxLineWidth { maxLineWidth = maxFoundLineWidth } @@ -274,15 +281,11 @@ public class TextLayoutManager: NSObject { delegate?.layoutManagerYAdjustment(yContentAdjustment) } - #if DEBUG - isInLayout = false - #endif - needsLayout = false - - // This needs to happen after ``needsLayout`` is toggled. Things can be triggered by frame changes. if originalHeight != lineStorage.height || layoutView?.frame.size.height != lineStorage.height { delegate?.layoutManagerHeightDidUpdate(newHeight: lineStorage.height) } + + needsLayout = false } /// Lays out a single text line. diff --git a/Sources/CodeEditTextView/TextView/TextView+Delete.swift b/Sources/CodeEditTextView/TextView/TextView+Delete.swift index 2691c37f..fe4363df 100644 --- a/Sources/CodeEditTextView/TextView/TextView+Delete.swift +++ b/Sources/CodeEditTextView/TextView/TextView+Delete.swift @@ -60,6 +60,7 @@ extension TextView { guard extendedRange.location >= 0 else { continue } textSelection.range.formUnion(extendedRange) } + selectionManager.textSelections.sort(by: { $0.range.location < $1.range.location }) KillRing.shared.kill( strings: selectionManager.textSelections.map(\.range).compactMap({ textStorage.substring(from: $0) }) ) diff --git a/Sources/CodeEditTextView/TextView/TextView+Layout.swift b/Sources/CodeEditTextView/TextView/TextView+Layout.swift index a68d78be..038dd349 100644 --- a/Sources/CodeEditTextView/TextView/TextView+Layout.swift +++ b/Sources/CodeEditTextView/TextView/TextView+Layout.swift @@ -58,6 +58,8 @@ extension TextView { inputContext?.invalidateCharacterCoordinates() } + /// Updates the view's frame if needed depending on wrapping lines, a new maximum width, or changed available size. + /// - Returns: Whether or not the view was updated. @discardableResult public func updateFrameIfNeeded() -> Bool { var availableSize = scrollView?.contentSize ?? .zero