Skip to content

Commit

Permalink
enumerateLineFragments in range
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Apr 5, 2024
1 parent 9db917d commit a2c485d
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 24 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# Glyph
Make life with TextKit better

This library is a light abstraction over TextKit. It provides some tools to make 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. 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.

## Installation

Expand All @@ -25,13 +25,16 @@ dependencies: [
### `NSTextContainer` Additions

```swift
func textSet(for 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)
```

### `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)
```

### `NSTextView`/`UITextView` Additions
Expand Down
52 changes: 42 additions & 10 deletions Sources/Glyph/NSTextContainer+Additions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@ extension NSTextContainer {
return layoutManager
}

func textRange(for rect: CGRect) -> NSRange? {
guard let layoutManager = nonDowngradingLayoutManager else { return nil }

let glyphRange = layoutManager.glyphRange(forBoundingRect: rect, in: self)

return layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
}

func tk1EnumerateLineFragments(for rect: CGRect, strictIntersection: Bool, block: (CGRect, NSRange, inout Bool) -> Void) {
guard let layoutManager = nonDowngradingLayoutManager else { return }

Expand Down Expand Up @@ -56,8 +48,6 @@ extension NSTextContainer {
}

textLayoutManager.enumerateLineFragments(for: rect) { fragmentRect, textRange, stop in
guard let textRange else { return }

let range = NSRange(textRange, provider: textContentManager)

block(fragmentRect, range, &stop)
Expand All @@ -68,6 +58,48 @@ extension NSTextContainer {

tk1EnumerateLineFragments(for: rect, strictIntersection: strictIntersection, block: block)
}

public func textSet(for rect: CGRect) -> IndexSet {
var set = IndexSet()

enumerateLineFragments(for: rect, strictIntersection: true) { _, range, _ in
set.insert(integersIn: range.lowerBound..<range.upperBound)
}

return set
}
}

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
}

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)
}
}
}
}
#endif

21 changes: 19 additions & 2 deletions Sources/Glyph/NSTextLayoutManager+Additions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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, NSTextRange, inout Bool) -> Void) {
// if this is nil, our optmizations will have no effect
let viewportRange = textViewportLayoutController.viewportRange ?? documentRange
let viewportBounds = textViewportLayoutController.viewportBounds
Expand Down Expand Up @@ -41,7 +41,7 @@ extension NSTextLayoutManager {

enumerateTextLayoutFragments(from: location, options: options, using: { fragment in
let frame = fragment.layoutFragmentFrame
let elementRange = fragment.textElement?.elementRange
let elementRange = fragment.rangeInElement

var keepGoing: Bool

Expand All @@ -61,5 +61,22 @@ extension NSTextLayoutManager {
})
}

public func enumerateLineFragments(in range: NSRange, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSTextRange, inout Bool) -> Void) {
let start = documentRange.location
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

var stop = false

block(frame, elementRange, &stop)

return stop == false && elementRange.endLocation.compare(end) == .orderedAscending
}
}
}
#endif
14 changes: 3 additions & 11 deletions Sources/Glyph/NSTextView+Additions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,11 @@ extension TextView {

/// Returns an IndexSet representing the content within `rect`.
public func textSet(for rect: CGRect) -> IndexSet {
var set = IndexSet()

#if os(macOS) && !targetEnvironment(macCatalyst)
guard let textContainer else {
return set
}
return textContainer?.textSet(for: rect) ?? IndexSet()
#elseif os(iOS) || os(visionOS)
return textContainer.textSet(for: rect)
#endif

textContainer.enumerateLineFragments(for: rect, strictIntersection: true) { _, range, _ in
set.insert(integersIn: range.lowerBound..<range.upperBound)
}

return set
}

/// Returns an IndexSet representing the visible content.
Expand Down

0 comments on commit a2c485d

Please sign in to comment.