-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fragment enumeration and visible set computation
- Loading branch information
1 parent
727cb92
commit f245683
Showing
7 changed files
with
243 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
#if os(macOS) && !targetEnvironment(macCatalyst) | ||
import AppKit | ||
#elseif os(iOS) || os(visionOS) | ||
import UIKit | ||
#endif | ||
|
||
#if os(macOS) || os(iOS) || os(visionOS) | ||
extension NSLayoutManager { | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
#if os(macOS) && !targetEnvironment(macCatalyst) | ||
import AppKit | ||
#elseif os(iOS) || os(visionOS) | ||
import UIKit | ||
#endif | ||
|
||
#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 | ||
} | ||
|
||
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 } | ||
|
||
let glyphRange = layoutManager.glyphRange(forBoundingRect: rect, in: self) | ||
|
||
withoutActuallyEscaping(block) { escapingBlock in | ||
layoutManager.enumerateLineFragments(forGlyphRange: glyphRange) { (fragmentRect, _, _, fragmentRange, stop) in | ||
var innerStop = false | ||
|
||
if strictIntersection { | ||
let intersectingRect = fragmentRect.intersection(rect) | ||
let intersectingGlyphRange = layoutManager.glyphRange(forBoundingRectWithoutAdditionalLayout: intersectingRect, in: self) | ||
let intersectingRange = layoutManager.characterRange(forGlyphRange: intersectingGlyphRange, actualGlyphRange: nil) | ||
|
||
escapingBlock(intersectingRect, intersectingRange, &innerStop) | ||
} else { | ||
escapingBlock(fragmentRect, fragmentRange, &innerStop) | ||
} | ||
|
||
stop.pointee = ObjCBool(innerStop) | ||
} | ||
} | ||
} | ||
|
||
/// Enumerate the line fragments that intersect a rect. | ||
/// | ||
/// - 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 | ||
guard let textRange else { return } | ||
|
||
let range = NSRange(textRange, provider: textContentManager) | ||
|
||
block(fragmentRect, range, &stop) | ||
} | ||
|
||
return | ||
} | ||
|
||
tk1EnumerateLineFragments(for: rect, strictIntersection: strictIntersection, block: block) | ||
} | ||
} | ||
#endif | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
#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 NSTextLayoutManager { | ||
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 | ||
let reversed = options.contains(.reverse) | ||
|
||
// we're going to start at a document limit, which is definitely correct but suboptimal | ||
|
||
var location: NSTextLocation | ||
|
||
if reversed { | ||
location = documentRange.endLocation | ||
|
||
if rect.maxY <= viewportBounds.maxY { | ||
location = viewportRange.endLocation | ||
} | ||
|
||
if rect.maxY <= viewportBounds.minY { | ||
location = viewportRange.location | ||
} | ||
} else { | ||
location = documentRange.location | ||
|
||
if rect.minY >= viewportBounds.minY { | ||
location = viewportRange.location | ||
} | ||
|
||
if rect.minY >= viewportBounds.maxY { | ||
location = viewportRange.endLocation | ||
} | ||
} | ||
|
||
enumerateTextLayoutFragments(from: location, options: options, using: { fragment in | ||
let frame = fragment.layoutFragmentFrame | ||
let elementRange = fragment.textElement?.elementRange | ||
|
||
var keepGoing: Bool | ||
|
||
if reversed { | ||
keepGoing = frame.minY < rect.minY | ||
} else { | ||
keepGoing = frame.maxY < rect.maxY | ||
} | ||
|
||
if keepGoing == false { | ||
return false | ||
} | ||
|
||
block(frame, elementRange, &keepGoing) | ||
|
||
return keepGoing | ||
}) | ||
} | ||
|
||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
#if os(macOS) && !targetEnvironment(macCatalyst) | ||
import AppKit | ||
#elseif os(iOS) || os(visionOS) | ||
import UIKit | ||
#endif | ||
|
||
// Taken from https://github.com/chimeHQ/Rearrange | ||
|
||
#if os(macOS) || os(iOS) || os(visionOS) | ||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, *) | ||
extension NSRange { | ||
init(_ textRange: NSTextRange, provider: NSTextElementProvider) { | ||
let docLocation = provider.documentRange.location | ||
|
||
let start = provider.offset?(from: docLocation, to: textRange.location) ?? NSNotFound | ||
if start == NSNotFound { | ||
self.init(location: start, length: 0) | ||
return | ||
} | ||
|
||
let end = provider.offset?(from: docLocation, to: textRange.endLocation) ?? NSNotFound | ||
if end == NSNotFound { | ||
self.init(location: NSNotFound, length: 0) | ||
return | ||
} | ||
|
||
self.init(start..<end) | ||
} | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
#if os(macOS) && !targetEnvironment(macCatalyst) | ||
import AppKit | ||
|
||
typealias TextView = NSTextView | ||
#elseif os(iOS) || os(visionOS) | ||
import UIKit | ||
|
||
typealias TextView = UITextView | ||
#endif | ||
|
||
extension TextView { | ||
Check failure on line 11 in Sources/Glyph/NSTextView+Additions.swift GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)
|
||
var visibleContainerRect: CGRect { | ||
#if os(macOS) && !targetEnvironment(macCatalyst) | ||
let origin = textContainerOrigin | ||
return visibleRect.offsetBy(dx: -origin.x, dy: -origin.y) | ||
#elseif os(iOS) || os(visionOS) | ||
return CGRect(origin: contentOffset, size: bounds.size) | ||
#endif | ||
|
||
} | ||
|
||
/// Returns an IndexSet representing the content within `rect`. | ||
public func textSet(for rect: CGRect) -> IndexSet { | ||
Check failure on line 23 in Sources/Glyph/NSTextView+Additions.swift GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)
|
||
var set = IndexSet() | ||
|
||
#if os(macOS) && !targetEnvironment(macCatalyst) | ||
guard let textContainer else { | ||
return set | ||
} | ||
#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. | ||
public var visibleTextSet: IndexSet { | ||
textSet(for: visibleContainerRect) | ||
} | ||
} |