Skip to content

Commit

Permalink
make use of NSTextLayoutFragment enumeration
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Apr 5, 2024
1 parent a2c485d commit d81d888
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 37 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
45 changes: 19 additions & 26 deletions Sources/Glyph/NSTextContainer+Additions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

Expand All @@ -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()

Expand All @@ -71,35 +66,33 @@ 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
}

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

29 changes: 29 additions & 0 deletions Sources/Glyph/NSTextLayoutFragment+Additions.swift
Original file line number Diff line number Diff line change
@@ -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
24 changes: 16 additions & 8 deletions Sources/Glyph/NSTextLayoutManager+Additions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
}
Expand Down

0 comments on commit d81d888

Please sign in to comment.