Skip to content

Commit

Permalink
bounding rects and NSRange-NSTextRange
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Aug 27, 2024
1 parent 8e80193 commit 4d73763
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 11 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
36 changes: 27 additions & 9 deletions Sources/Glyph/NSTextContainer+Additions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -95,4 +87,30 @@ extension NSTextContainer {
tk1EnumerateLineFragments(in: range, block: block)
}
}

extension NSTextContainer {
public func boundingRect(for range: NSRange) -> NSRect? {

Check failure on line 92 in Sources/Glyph/NSTextContainer+Additions.swift

View workflow job for this annotation

GitHub Actions / Test (platform=visionOS Simulator,name=Apple Vision Pro)

cannot find type 'NSRect' in scope

Check failure on line 92 in Sources/Glyph/NSTextContainer+Additions.swift

View workflow job for this annotation

GitHub Actions / Test (platform=visionOS Simulator,name=Apple Vision Pro)

cannot find type 'NSRect' in scope
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? {

Check failure on line 100 in Sources/Glyph/NSTextContainer+Additions.swift

View workflow job for this annotation

GitHub Actions / Test (platform=visionOS Simulator,name=Apple Vision Pro)

cannot find type 'NSRect' in scope

Check failure on line 100 in Sources/Glyph/NSTextContainer+Additions.swift

View workflow job for this annotation

GitHub Actions / Test (platform=visionOS Simulator,name=Apple Vision Pro)

cannot find type 'NSRect' in scope
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
44 changes: 43 additions & 1 deletion Sources/Glyph/NSTextLayoutManager+Additions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -85,5 +117,15 @@ extension NSTextLayoutManager {
return stop == false && fragmentRange.endLocation.compare(end) == .orderedAscending
}
}

func boundingRect(for range: NSRange) -> NSRect? {

Check failure on line 121 in Sources/Glyph/NSTextLayoutManager+Additions.swift

View workflow job for this annotation

GitHub Actions / Test (platform=visionOS Simulator,name=Apple Vision Pro)

cannot find type 'NSRect' in scope

Check failure on line 121 in Sources/Glyph/NSTextLayoutManager+Additions.swift

View workflow job for this annotation

GitHub Actions / Test (platform=visionOS Simulator,name=Apple Vision Pro)

cannot find type 'NSRect' in scope
var rect: NSRect? = nil

Check failure on line 122 in Sources/Glyph/NSTextLayoutManager+Additions.swift

View workflow job for this annotation

GitHub Actions / Test (platform=visionOS Simulator,name=Apple Vision Pro)

cannot find type 'NSRect' in scope

enumerateTextLineFragments(in: range, options: [.ensuresLayout]) { lineFragment, lineRect, lineRange, stop in
rect = rect?.union(lineRect) ?? lineRect
}

return rect
}
}
#endif
48 changes: 48 additions & 0 deletions Sources/Glyph/NSTextRange+NSRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -26,5 +51,28 @@ extension NSRange {

self.init(start..<end)
}

public init?(_ textRange: NSTextRange) {
guard
let start = textRange.location as? UTF16TextLocation,
let end = textRange.endLocation as? UTF16TextLocation
else {
return nil
}

self.init(start.value..<end.value)
}
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, *)
@available(watchOS, unavailable)
extension NSTextRange {
public convenience init?(_ range: NSRange) {
let start = UTF16TextLocation(value: range.lowerBound)
let end = UTF16TextLocation(value: range.upperBound)

self.init(location: start, end: end)
}
}

#endif
17 changes: 17 additions & 0 deletions Sources/Glyph/NSTextView+Additions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,22 @@ extension TextView {
public var visibleCharacterIndexes: IndexSet {
characterIndexes(within: visibleContainerRect)
}

/// Returns the bounding rectangle for the given text range.
public func boundingRect(for range: NSRange) -> NSRect? {

Check failure on line 38 in Sources/Glyph/NSTextView+Additions.swift

View workflow job for this annotation

GitHub Actions / Test (platform=visionOS Simulator,name=Apple Vision Pro)

cannot find type 'NSRect' in scope
#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

Check failure on line 49 in Sources/Glyph/NSTextView+Additions.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS,variant=Mac Catalyst)

cannot find 'textContainerOrigin' in scope

return rect.offsetBy(dx: origin.x, dy: origin.y)
}
}
#endif

0 comments on commit 4d73763

Please sign in to comment.