diff --git a/README.md b/README.md index bdc2ba6..281aa38 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,17 @@ 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 enumerateLineFragments(from index: Int, forward: Bool = true, block: (CGRect, NSRange, inout Bool) -> Void) +func lineFragment(after index: Int, forward: Bool = true) -> (CGRect, NSRange)? func boundingRect(for range: NSRange) -> CGRect? ``` ### `NSTextLayoutManager` Additions ```swift -func enumerateLineFragments(for rect: CGRect, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSRange, inout Bool) -> Void) +func enumerateLineFragments(for rect: CGRect, strictIntersection: Bool = true, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSRange, inout Bool) -> Void) func enumerateLineFragments(in range: NSRange, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSRange, inout Bool) -> Void) +func enumerateLineFragments(from index: Int, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSRange, inout Bool) -> Void) ``` ### `NSTextLayoutFragment` Additions @@ -63,7 +66,7 @@ NSTextRange.init?(_ range: NSRange) ## Contributing and Collaboration -I would love to hear from you! Issues or pull requests work great. A [Matrix space][matrix] is also available for live help, but I have a strong bias towards answering in the form of documentation. +I would love to hear from you! Issues or pull requests work great. Both a [Matrix space][matrix] and [Discord][discord] are available for live help, but I have a strong bias towards answering in the form of documentation. You can also find me [here](https://www.massicotte.org/about). I prefer collaboration, and would love to find ways to work together if you have a similar project. @@ -79,3 +82,4 @@ By participating in this project you agree to abide by the [Contributor Code of [documentation badge]: https://img.shields.io/badge/Documentation-DocC-blue [matrix]: https://matrix.to/#/%23chimehq%3Amatrix.org [matrix badge]: https://img.shields.io/matrix/chimehq%3Amatrix.org?label=Matrix +[discord]: https://discord.gg/esFpX6sErJ diff --git a/Sources/Glyph/NSLayoutManager+Additions.swift b/Sources/Glyph/NSLayoutManager+Additions.swift index 1885956..1a1e031 100644 --- a/Sources/Glyph/NSLayoutManager+Additions.swift +++ b/Sources/Glyph/NSLayoutManager+Additions.swift @@ -6,5 +6,27 @@ import UIKit #if os(macOS) || os(iOS) || os(visionOS) extension NSLayoutManager { + func enumerateLineFragments(for rect: CGRect, in container: NSTextContainer, strictIntersection: Bool, block: (CGRect, NSRange, inout Bool) -> Void) { + let glRange = glyphRange(forBoundingRect: rect, in: container) + + withoutActuallyEscaping(block) { escapingBlock in + enumerateLineFragments(forGlyphRange: glRange) { (fragmentRect, _, _, fragmentRange, stop) in + var innerStop = false + + if strictIntersection { + let intersectingRect = fragmentRect.intersection(rect) + let intersectingGlyphRange = self.glyphRange(forBoundingRectWithoutAdditionalLayout: intersectingRect, in: container) + let intersectingRange = self.characterRange(forGlyphRange: intersectingGlyphRange, actualGlyphRange: nil) + + escapingBlock(intersectingRect, intersectingRange, &innerStop) + } else { + escapingBlock(fragmentRect, fragmentRange, &innerStop) + } + + stop.pointee = ObjCBool(innerStop) + } + } + + } } #endif diff --git a/Sources/Glyph/NSTextContainer+Additions.swift b/Sources/Glyph/NSTextContainer+Additions.swift index c44db4d..baddab8 100644 --- a/Sources/Glyph/NSTextContainer+Additions.swift +++ b/Sources/Glyph/NSTextContainer+Additions.swift @@ -6,45 +6,23 @@ import UIKit #if os(macOS) || os(iOS) || os(visionOS) extension NSTextContainer { - private func tk1EnumerateLineFragments(for rect: CGRect, strictIntersection: Bool, block: (CGRect, NSRange, inout Bool) -> Void) { - guard let layoutManager = layoutManager 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 { - textLayoutManager.enumerateLineFragments(for: rect) { fragmentRect, range, stop in + textLayoutManager.enumerateLineFragments(for: rect, strictIntersection: strictIntersection) { fragmentRect, range, stop in block(fragmentRect, range, &stop) } return } - tk1EnumerateLineFragments(for: rect, strictIntersection: strictIntersection, block: block) + layoutManager?.enumerateLineFragments(for: rect, in: self, strictIntersection: strictIntersection, block: block) } +} +extension NSTextContainer { /// Returns an IndexSet representing the content within `rect`. public func characterIndexes(within rect: CGRect) -> IndexSet { var set = IndexSet() @@ -86,6 +64,45 @@ extension NSTextContainer { tk1EnumerateLineFragments(in: range, block: block) } + + private func tk1EnumerateLineFragments(from index: Int, forward: Bool, block: (CGRect, NSRange, inout Bool) -> Void) { + // TODO + } + + public func enumerateLineFragments(from index: Int, forward: Bool = true, block: (CGRect, NSRange, inout Bool) -> Void) { + if #available(macOS 12.0, iOS 15.0, *), let textLayoutManager { + let options: NSTextLayoutFragment.EnumerationOptions = forward ? [.ensuresLayout] : [.reverse, .ensuresLayout] + + textLayoutManager.enumerateLineFragments(from: index, options: options, block: block) + + return + } + + tk1EnumerateLineFragments(from: index, forward: forward, block: block) + } + + /// Find line fragment details immediately above or below a character index. + public func lineFragment(after index: Int, forward: Bool = true) -> (CGRect, NSRange)? { + var pairs: [(CGRect, NSRange)] = [] + + enumerateLineFragments(from: index, forward: forward) { rect, range, stop in + if pairs.count == 2 { + stop = true + return + } + + pairs.append((rect, range)) + } + + guard + pairs.count == 2, + let lastLine = pairs.last + else { + return nil + } + + return lastLine + } } extension NSTextContainer { @@ -104,13 +121,5 @@ extension NSTextContainer { 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 diff --git a/Sources/Glyph/NSTextLayoutFragment+Additions.swift b/Sources/Glyph/NSTextLayoutFragment+Additions.swift index 6a90ab4..86a5a79 100644 --- a/Sources/Glyph/NSTextLayoutFragment+Additions.swift +++ b/Sources/Glyph/NSTextLayoutFragment+Additions.swift @@ -5,6 +5,53 @@ import UIKit #endif #if os(macOS) || os(iOS) || os(visionOS) +@available(macOS 12.0, iOS 15.0, *) +extension NSTextLineFragment { + /// span has to be within this fragment's coordinate system + func rangeOfCharacters(intersecting span: Range) -> NSRange? { + let length = characterRange.length + + var start: Int? + + for index in 0.. point.x { + end = min(index + 1, length) + break + } + + if span.upperBound == point.x { + end = index + break + } + } + + guard let end else { return nil } + + return NSRange(start.. Void) { @@ -25,5 +72,34 @@ extension NSTextLayoutFragment { block(textLineFragment, bounds, range) } } + + func enumerateLineFragments( + with provider: NSTextElementProvider, + intersecting rect: CGRect, + 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 overlap = bounds.intersection(rect) + let span: Range = overlap.minX.. Void) { + public func enumerateLineFragments(for rect: CGRect, strictIntersection: Bool = true, options: NSTextLayoutFragment.EnumerationOptions = [], block: (CGRect, NSRange, inout Bool) -> Void) { guard let textContentManager else { return } // if this is nil, our optmizations will have no effect @@ -44,20 +44,25 @@ extension NSTextLayoutManager { enumerateTextLayoutFragments(from: location, options: options, using: { fragment in let frame = fragment.layoutFragmentFrame - var keepGoing: Bool - - if reversed { - keepGoing = frame.minY < rect.minY - } else { - keepGoing = frame.maxY < rect.maxY + if frame.intersects(rect) == false { + // if we don't intersect, perhaps we just haven't reached window yet? + if reversed { + return frame.minY < rect.minY + } else { + return frame.maxY < rect.maxY + } } - if keepGoing == false { - return false - } + var keepGoing: Bool = true - fragment.enumerateLineFragments(with: textContentManager) { _, frame, elementRange in - block(frame, elementRange, &keepGoing) + if strictIntersection { + fragment.enumerateLineFragments(with: textContentManager, intersecting: rect) { _, lineFrame, elementRange in + block(lineFrame, elementRange, &keepGoing) + } + } else { + fragment.enumerateLineFragments(with: textContentManager) { _, lineFrame, elementRange in + block(lineFrame, elementRange, &keepGoing) + } } return keepGoing @@ -99,6 +104,7 @@ extension NSTextLayoutManager { ) { guard let textContentManager else { return } + // pretty sure this is a bug, range.location needs to be used no? let start = documentRange.location guard let end = textContentManager.location(start, offsetBy: range.length) else { return @@ -118,6 +124,30 @@ extension NSTextLayoutManager { } } + public func enumerateLineFragments( + from index: Int, + options: NSTextLayoutFragment.EnumerationOptions = [], + block: (CGRect, NSRange, inout Bool) -> Void + ) { + guard let textContentManager else { return } + + let docStart = documentRange.location + guard let start = textContentManager.location(docStart, offsetBy: index) else { + return + } + + enumerateTextLayoutFragments(from: start, options: options) { fragment in + var stop = false + + fragment.enumerateLineFragments(with: textContentManager) { _, frame, elementRange in + block(frame, elementRange, &stop) + } + + + return stop == false + } + } + func boundingRect(for range: NSRange) -> CGRect? { var rect: CGRect? = nil