diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index bfb6084f..8f47a1ae 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -195,15 +195,38 @@ class ContentTextView: LinkTextView, BaseEmojiLabel { func getLinkAtPoint(_ point: CGPoint) -> (URL, NSRange)? { let locationInTextContainer = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top) - var partialFraction: CGFloat = 0 - let characterIndex = layoutManager.characterIndex(for: locationInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &partialFraction) - if characterIndex < textStorage.length && partialFraction < 1 { - var range = NSRange() - if let link = textStorage.attribute(.link, at: characterIndex, longestEffectiveRange: &range, in: textStorage.fullRange) as? URL { - return (link, range) + if #available(iOS 16.0, *), + let textLayoutManager { + guard let fragment = textLayoutManager.textLayoutFragment(for: point), + let lineFragment = fragment.textLineFragments.first(where: { lineFragment in + lineFragment.typographicBounds.offsetBy(dx: fragment.layoutFragmentFrame.minX, dy: fragment.layoutFragmentFrame.minY).contains(point) + }) else { + return nil } + let charIndex = lineFragment.characterIndex(for: point) + + var range = NSRange() + guard let link = lineFragment.attributedString.attribute(.link, at: charIndex, longestEffectiveRange: &range, in: lineFragment.attributedString.fullRange) as? URL else { + return nil + } + // lineFragment.attributedString is the NSTextLayoutFragment's string, and so range is in its index space + // but we need to return a range in our whole attributedString's space, so convert it + let textLayoutFragmentStart = textLayoutManager.offset(from: textLayoutManager.documentRange.location, to: fragment.rangeInElement.location) + let rangeInSelf = NSRange(location: range.location + textLayoutFragmentStart, length: range.length) + return (link, rangeInSelf) + } else { + var partialFraction: CGFloat = 0 + let characterIndex = layoutManager.characterIndex(for: locationInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: &partialFraction) + guard characterIndex < textStorage.length && partialFraction < 1 else { + return nil + } + + var range = NSRange() + guard let link = textStorage.attribute(.link, at: characterIndex, longestEffectiveRange: &range, in: textStorage.fullRange) as? URL else { + return nil + } + return (link, range) } - return nil } func handleLinkTapped(url: URL, text: String) { @@ -297,8 +320,25 @@ extension ContentTextView: UIContextMenuInteractionDelegate { // Determine the line rects that the link takes up in the coordinate space of this view. var rects = [CGRect]() - layoutManager.enumerateEnclosingRects(forGlyphRange: range, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { (rect, stop) in - rects.append(rect) + if #available(iOS 16.0, *), + let textLayoutManager, + let contentManager = textLayoutManager.textContentManager { + // convert from NSRange to NSTextRange + // i have no idea under what circumstances any of these calls could fail + guard let startLoc = contentManager.location(contentManager.documentRange.location, offsetBy: range.location), + let endLoc = contentManager.location(startLoc, offsetBy: range.length), + let textRange = NSTextRange(location: startLoc, end: endLoc) else { + return nil + } + // .standard because i have no idea what the difference is + textLayoutManager.enumerateTextSegments(in: textRange, type: .standard, options: []) { range, rect, float, textContainer in + rects.append(rect) + return true + } + } else { + layoutManager.enumerateEnclosingRects(forGlyphRange: range, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { (rect, stop) in + rects.append(rect) + } } // Try to create a snapshot view of this view to disply as the preview.