From 4d73763a66c3bc17ab0b6bdbde4dabd5ba8fb89d Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Tue, 27 Aug 2024 06:17:07 -0400 Subject: [PATCH] bounding rects and NSRange-NSTextRange --- README.md | 11 ++++- Sources/Glyph/NSTextContainer+Additions.swift | 36 ++++++++++---- .../Glyph/NSTextLayoutManager+Additions.swift | 44 ++++++++++++++++- Sources/Glyph/NSTextRange+NSRange.swift | 48 +++++++++++++++++++ Sources/Glyph/NSTextView+Additions.swift | 17 +++++++ 5 files changed, 145 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c551458..e9fcd58 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ dependencies: [ func characterIndexes(within rect: CGRect) -> IndexSet func enumerateLineFragments(for rect: CGRect, strictIntersection: Bool, block: (CGRect, NSRange, inout Bool) -> Void) func enumerateLineFragments(in range: NSRange, block: (CGRect, NSRange, inout Bool) -> Void) +func boundingRect(for range: NSRange) -> NSRect? ``` ### `NSTextLayoutManager` Additions @@ -45,9 +46,17 @@ func enumerateLineFragments(with provider: NSTextElementProvider, block: (NSText ### `NSTextView`/`UITextView` Additions -``` +```swift func characterIndexes(within rect: CGRect) -> IndexSet var visibleCharacterIndexes: IndexSet +func boundingRect(for range: NSRange) -> NSRect? +``` + +### `NSRange` and `NSTextRange` Additions + +```swift +NSRange.init?(_ textRange: NSTextRange) +NSTextRange.init?(_ range: NSRange) ``` ## Contributing and Collaboration diff --git a/Sources/Glyph/NSTextContainer+Additions.swift b/Sources/Glyph/NSTextContainer+Additions.swift index 9c123da..229b2b1 100644 --- a/Sources/Glyph/NSTextContainer+Additions.swift +++ b/Sources/Glyph/NSTextContainer+Additions.swift @@ -6,16 +6,8 @@ import UIKit #if os(macOS) || os(iOS) || os(visionOS) extension NSTextContainer { - var nonDowngradingLayoutManager: NSLayoutManager? { - if #available(macOS 12.0, iOS 15.0, *), textLayoutManager != nil { - return nil - } - - return layoutManager - } - private func tk1EnumerateLineFragments(for rect: CGRect, strictIntersection: Bool, block: (CGRect, NSRange, inout Bool) -> Void) { - guard let layoutManager = nonDowngradingLayoutManager else { return } + guard let layoutManager = layoutManager else { return } let glyphRange = layoutManager.glyphRange(forBoundingRect: rect, in: self) @@ -95,4 +87,30 @@ extension NSTextContainer { tk1EnumerateLineFragments(in: range, block: block) } } + +extension NSTextContainer { + public func boundingRect(for range: NSRange) -> NSRect? { + if #available(macOS 12.0, iOS 15.0, *), let textLayoutManager { + return textLayoutManager.boundingRect(for: range) + } + + return tk1BoundingRect(for: range) + } + + private func tk1BoundingRect(for range: NSRange) -> NSRect? { + guard let layoutManager else { return nil } + + let glyphRange = layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil) + + return layoutManager.boundingRect(forGlyphRange: glyphRange, in: self) + } + + private func tk1TextRange(intersecting rect: CGRect) -> NSRange? { + guard let layoutManager else { return nil } + + let glyphRange = layoutManager.glyphRange(forBoundingRect: rect, in: self) + + return layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) + } +} #endif diff --git a/Sources/Glyph/NSTextLayoutManager+Additions.swift b/Sources/Glyph/NSTextLayoutManager+Additions.swift index 907778c..ac9f89c 100644 --- a/Sources/Glyph/NSTextLayoutManager+Additions.swift +++ b/Sources/Glyph/NSTextLayoutManager+Additions.swift @@ -64,7 +64,39 @@ extension NSTextLayoutManager { }) } - public func enumerateLineFragments(in range: NSRange, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSRange, inout Bool) -> Void) { + private func enumerateTextLineFragments( + in range: NSRange, + options: NSTextLayoutFragment.EnumerationOptions = [], + block: (NSTextLineFragment, CGRect, NSRange, inout Bool) -> Void + ) { + guard let textContentManager else { return } + + let docStart = documentRange.location + guard + let start = textContentManager.location(docStart, offsetBy: range.lowerBound), + let end = textContentManager.location(docStart, offsetBy: range.upperBound) + else { + return + } + + enumerateTextLayoutFragments(from: start, options: options) { fragment in + let fragmentRange = fragment.rangeInElement + + var stop = false + + fragment.enumerateLineFragments(with: textContentManager) { lineFragment, frame, elementRange in + block(lineFragment, frame, elementRange, &stop) + } + + return stop == false && fragmentRange.endLocation.compare(end) == .orderedAscending + } + } + + public func enumerateLineFragments( + in range: NSRange, + options: NSTextLayoutFragment.EnumerationOptions = [], + block: (CGRect, NSRange, inout Bool) -> Void + ) { guard let textContentManager else { return } let start = documentRange.location @@ -85,5 +117,15 @@ extension NSTextLayoutManager { return stop == false && fragmentRange.endLocation.compare(end) == .orderedAscending } } + + func boundingRect(for range: NSRange) -> NSRect? { + var rect: NSRect? = nil + + enumerateTextLineFragments(in: range, options: [.ensuresLayout]) { lineFragment, lineRect, lineRange, stop in + rect = rect?.union(lineRect) ?? lineRect + } + + return rect + } } #endif diff --git a/Sources/Glyph/NSTextRange+NSRange.swift b/Sources/Glyph/NSTextRange+NSRange.swift index 89d106e..369045a 100644 --- a/Sources/Glyph/NSTextRange+NSRange.swift +++ b/Sources/Glyph/NSTextRange+NSRange.swift @@ -7,6 +7,31 @@ import UIKit // Taken from https://github.com/chimeHQ/Rearrange #if os(macOS) || os(iOS) || os(visionOS) +@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) +final class UTF16TextLocation: NSObject, NSTextLocation { + let value: Int + + init(value: Int) { + self.value = value + } + + func compare(_ location: any NSTextLocation) -> ComparisonResult { + guard let utf16Loc = location as? UTF16TextLocation else { + return .orderedSame + } + + if value < utf16Loc.value { + return .orderedAscending + } + + if value > utf16Loc.value { + return .orderedDescending + } + + return .orderedSame + } +} + @available(iOS 15.0, macOS 12.0, tvOS 15.0, *) extension NSRange { init(_ textRange: NSTextRange, provider: NSTextElementProvider) { @@ -26,5 +51,28 @@ extension NSRange { self.init(start.. NSRect? { +#if os(macOS) && !targetEnvironment(macCatalyst) + guard let rect = textContainer?.boundingRect(for: range) else { + return nil + } +#elseif os(iOS) || os(visionOS) + guard let rect = textContainer.boundingRect(for: range) else { + return nil + } +#endif + + let origin = textContainerOrigin + + return rect.offsetBy(dx: origin.x, dy: origin.y) + } } #endif