diff --git a/README.md b/README.md index 4e9aa06..3db556b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ # Glyph Make life with TextKit better -Glyph adds features and abstractions for working with TextKit. It makes it easier and more efficient to work with. You don't even need to know whether your view using 1 or 2, and glyph will not downgrade TextKit 2 views. +Glyph adds features and abstractions for working with TextKit. Some are for performance, some for convenience. You don't even need to know whether your system using 1 or 2. Glyph will not downgrade TextKit 2 views. But, it can be awful nice to swap between 1 and 2 quickly when debugging. ## Installation @@ -33,8 +33,14 @@ func enumerateLineFragments(in range: NSRange, block: (CGRect, NSRange, inout Bo ### `NSTextLayoutManager` Additions ```swift -func enumerateLineFragments(for rect: CGRect, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSTextRange?, inout Bool) -> Void) -func enumerateLineFragments(in range: NSRange, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSTextRange, inout Bool) -> Void) +func enumerateLineFragments(for rect: CGRect, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSRange, inout Bool) -> Void) +func enumerateLineFragments(in range: NSRange, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSRange, inout Bool) -> Void) +``` + +### `NSTextLayoutFragment` Additions + +```swift +func enumerateLineFragments(with provider: NSTextElementProvider, block: (NSTextLineFragment, CGRect, NSRange) -> Void) ``` ### `NSTextView`/`UITextView` Additions diff --git a/Sources/Glyph/NSTextContainer+Additions.swift b/Sources/Glyph/NSTextContainer+Additions.swift index a293bfe..4603c56 100644 --- a/Sources/Glyph/NSTextContainer+Additions.swift +++ b/Sources/Glyph/NSTextContainer+Additions.swift @@ -14,7 +14,7 @@ extension NSTextContainer { return layoutManager } - func tk1EnumerateLineFragments(for rect: CGRect, strictIntersection: Bool, block: (CGRect, NSRange, inout Bool) -> Void) { + private func tk1EnumerateLineFragments(for rect: CGRect, strictIntersection: Bool, block: (CGRect, NSRange, inout Bool) -> Void) { guard let layoutManager = nonDowngradingLayoutManager else { return } let glyphRange = layoutManager.glyphRange(forBoundingRect: rect, in: self) @@ -43,13 +43,7 @@ extension NSTextContainer { /// - Parameter strictIntersection: If true, the result will only be rect and range strictly within the `rect` parameter. This is more expensive to compute. public func enumerateLineFragments(for rect: CGRect, strictIntersection: Bool, block: (CGRect, NSRange, inout Bool) -> Void) { if #available(macOS 12.0, iOS 15.0, *), let textLayoutManager { - guard let textContentManager = textLayoutManager.textContentManager else { - return - } - - textLayoutManager.enumerateLineFragments(for: rect) { fragmentRect, textRange, stop in - let range = NSRange(textRange, provider: textContentManager) - + textLayoutManager.enumerateLineFragments(for: rect) { fragmentRect, range, stop in block(fragmentRect, range, &stop) } @@ -59,6 +53,7 @@ extension NSTextContainer { tk1EnumerateLineFragments(for: rect, strictIntersection: strictIntersection, block: block) } + /// Returns an IndexSet representing the content within `rect`. public func textSet(for rect: CGRect) -> IndexSet { var set = IndexSet() @@ -71,21 +66,7 @@ extension NSTextContainer { } extension NSTextContainer { - public func enumerateLineFragments(in range: NSRange, block: (CGRect, NSRange, inout Bool) -> Void) { - if #available(macOS 12.0, iOS 15.0, *), let textLayoutManager { - guard let textContentManager = textLayoutManager.textContentManager else { - return - } - - textLayoutManager.enumerateLineFragments(in: range) { fragmentRect, textRange, stop in - let range = NSRange(textRange, provider: textContentManager) - - block(fragmentRect, range, &stop) - } - - return - } - + private func tk1EnumerateLineFragments(in range: NSRange, block: (CGRect, NSRange, inout Bool) -> Void) { guard let glyphRange = layoutManager?.glyphRange(forCharacterRange: range, actualCharacterRange: nil) else { return } @@ -93,13 +74,25 @@ extension NSTextContainer { withoutActuallyEscaping(block) { escapingBlock in layoutManager?.enumerateLineFragments(forGlyphRange: glyphRange) { (fragmentRect, _, _, fragmentRange, stop) in var innerStop = false - + escapingBlock(fragmentRect, fragmentRange, &innerStop) - + stop.pointee = ObjCBool(innerStop) } } } + + /// Enumerate the line fragments that intersect `range`. + public func enumerateLineFragments(in range: NSRange, block: (CGRect, NSRange, inout Bool) -> Void) { + if #available(macOS 12.0, iOS 15.0, *), let textLayoutManager { + textLayoutManager.enumerateLineFragments(in: range) { fragmentRect, fragmentRange, stop in + block(fragmentRect, fragmentRange, &stop) + } + + return + } + + tk1EnumerateLineFragments(in: range, block: block) + } } #endif - diff --git a/Sources/Glyph/NSTextLayoutFragment+Additions.swift b/Sources/Glyph/NSTextLayoutFragment+Additions.swift new file mode 100644 index 0000000..6a90ab4 --- /dev/null +++ b/Sources/Glyph/NSTextLayoutFragment+Additions.swift @@ -0,0 +1,29 @@ +#if os(macOS) && !targetEnvironment(macCatalyst) +import AppKit +#elseif os(iOS) || os(visionOS) +import UIKit +#endif + +#if os(macOS) || os(iOS) || os(visionOS) +@available(macOS 12.0, iOS 15.0, *) +extension NSTextLayoutFragment { + public func enumerateLineFragments(with provider: NSTextElementProvider, block: (NSTextLineFragment, CGRect, NSRange) -> Void) { + let origin = layoutFragmentFrame.origin + let location = provider.offset?(from: provider.documentRange.location, to: rangeInElement.location) ?? 0 + + // check to ensure our shift will always be valid + precondition(location >= 0) + precondition(location != NSNotFound) + + for textLineFragment in textLineFragments { + let bounds = textLineFragment.typographicBounds.offsetBy(dx: origin.x, dy: origin.y) + let range = NSRange( + location: textLineFragment.characterRange.location + location, + length: textLineFragment.characterRange.length + ) + + block(textLineFragment, bounds, range) + } + } +} +#endif diff --git a/Sources/Glyph/NSTextLayoutManager+Additions.swift b/Sources/Glyph/NSTextLayoutManager+Additions.swift index e130def..222c8d9 100644 --- a/Sources/Glyph/NSTextLayoutManager+Additions.swift +++ b/Sources/Glyph/NSTextLayoutManager+Additions.swift @@ -7,7 +7,9 @@ import UIKit #if os(macOS) || os(iOS) || os(visionOS) @available(macOS 12.0, iOS 15.0, *) extension NSTextLayoutManager { - public func enumerateLineFragments(for rect: CGRect, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSTextRange, inout Bool) -> Void) { + public func enumerateLineFragments(for rect: CGRect, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSRange, inout Bool) -> Void) { + guard let textContentManager else { return } + // if this is nil, our optmizations will have no effect let viewportRange = textViewportLayoutController.viewportRange ?? documentRange let viewportBounds = textViewportLayoutController.viewportBounds @@ -55,27 +57,33 @@ extension NSTextLayoutManager { return false } - block(frame, elementRange, &keepGoing) + fragment.enumerateLineFragments(with: textContentManager) { _, frame, elementRange in + block(frame, elementRange, &keepGoing) + } return keepGoing }) } - public func enumerateLineFragments(in range: NSRange, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSTextRange, inout Bool) -> Void) { + public func enumerateLineFragments(in range: NSRange, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSRange, inout Bool) -> Void) { + guard let textContentManager else { return } + let start = documentRange.location - guard let end = textContentManager?.location(start, offsetBy: range.length) else { + guard let end = textContentManager.location(start, offsetBy: range.length) else { return } enumerateTextLayoutFragments(from: documentRange.location, options: options) { fragment in - let frame = fragment.layoutFragmentFrame - let elementRange = fragment.rangeInElement + let fragmentRange = fragment.rangeInElement var stop = false - block(frame, elementRange, &stop) + fragment.enumerateLineFragments(with: textContentManager) { _, frame, elementRange in + block(frame, elementRange, &stop) + } + - return stop == false && elementRange.endLocation.compare(end) == .orderedAscending + return stop == false && fragmentRange.endLocation.compare(end) == .orderedAscending } } }