Improve text selection

This commit is contained in:
Ali 2019-12-24 13:54:51 +04:00
parent 2714cf2d19
commit cf29793404
2 changed files with 56 additions and 19 deletions

View File

@ -13,6 +13,18 @@ private final class TextNodeStrikethrough {
}
}
public struct TextRangeRectEdge: Equatable {
public var x: CGFloat
public var y: CGFloat
public var height: CGFloat
public init(x: CGFloat, y: CGFloat, height: CGFloat) {
self.x = x
self.y = y
self.height = height
}
}
private final class TextNodeLine {
let line: CTLine
let frame: CGRect
@ -589,11 +601,13 @@ public final class TextNodeLayout: NSObject {
return nil
}
public func rangeRects(in range: NSRange) -> [CGRect]? {
public func rangeRects(in range: NSRange) -> (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)? {
guard let _ = self.attributedString, range.length != 0 else {
return nil
}
var rects: [(CGRect, CGRect)] = []
var startEdge: TextRangeRectEdge?
var endEdge: TextRangeRectEdge?
for line in self.lines {
let lineRange = NSIntersectionRange(range, line.range)
if lineRange.length != 0 {
@ -616,11 +630,34 @@ public final class TextNodeLayout: NSObject {
let width = max(0.0, abs(rightOffset - leftOffset))
if line.range.contains(range.lowerBound) {
let offsetX = floor(CTLineGetOffsetForStringIndex(line.line, range.lowerBound, nil))
startEdge = TextRangeRectEdge(x: lineFrame.minX + offsetX, y: lineFrame.minY, height: lineFrame.height)
}
if line.range.contains(range.upperBound - 1) {
let offsetX: CGFloat
if line.range.upperBound == range.upperBound {
offsetX = lineFrame.maxX
} else {
var secondaryOffset: CGFloat = 0.0
let primaryOffset = floor(CTLineGetOffsetForStringIndex(line.line, range.upperBound - 1, &secondaryOffset))
secondaryOffset = floor(secondaryOffset)
let nextOffet = floor(CTLineGetOffsetForStringIndex(line.line, range.upperBound, &secondaryOffset))
if primaryOffset != secondaryOffset {
offsetX = secondaryOffset
} else {
offsetX = nextOffet
}
}
endEdge = TextRangeRectEdge(x: lineFrame.minX + offsetX, y: lineFrame.minY, height: lineFrame.height)
}
rects.append((lineFrame, CGRect(origin: CGPoint(x: lineFrame.minX + min(leftOffset, rightOffset) + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: width, height: lineFrame.size.height))))
}
}
if !rects.isEmpty {
return rects.map { $1 }
if !rects.isEmpty, let startEdge = startEdge, let endEdge = endEdge {
return (rects.map { $1 }, startEdge, endEdge)
}
return nil
}
@ -768,7 +805,7 @@ public class TextNode: ASDisplayNode {
}
}
public func rangeRects(in range: NSRange) -> [CGRect]? {
public func rangeRects(in range: NSRange) -> (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)? {
if let cachedLayout = self.cachedLayout {
return cachedLayout.rangeRects(in: range)
} else {

View File

@ -260,17 +260,17 @@ public final class TextSelectionNode: ASDisplayNode {
let mappedPoint = strongSelf.view.convert(point, to: strongSelf.textNode.view)
if let stringIndex = strongSelf.textNode.attributesAtPoint(mappedPoint, orNearest: true)?.0 {
var updatedMin = currentRange.0
var updatedMax = currentRange.1
var updatedLeft = currentRange.0
var updatedRight = currentRange.1
switch knob {
case .left:
updatedMin = stringIndex
updatedLeft = stringIndex
case .right:
updatedMax = stringIndex
updatedRight = stringIndex
}
let updatedRange = NSRange(location: min(updatedMin, updatedMax), length: max(updatedMin, updatedMax) - min(updatedMin, updatedMax))
if strongSelf.currentRange?.0 != updatedMin || strongSelf.currentRange?.1 != updatedMax {
strongSelf.currentRange = (updatedMin, updatedMax)
if strongSelf.currentRange?.0 != updatedLeft || strongSelf.currentRange?.1 != updatedRight {
strongSelf.currentRange = (updatedLeft, updatedRight)
let updatedRange = NSRange(location: min(updatedLeft, updatedRight), length: max(updatedLeft, updatedRight) - min(updatedLeft, updatedRight))
strongSelf.updateSelection(range: updatedRange, animateIn: false)
}
@ -301,12 +301,12 @@ public final class TextSelectionNode: ASDisplayNode {
let inputRange = CFRangeMake(0, string.length)
let flag = UInt(kCFStringTokenizerUnitWord)
let locale = CFLocaleCopyCurrent()
let tokenizer = CFStringTokenizerCreate( kCFAllocatorDefault, string as CFString, inputRange, flag, locale)
let tokenizer = CFStringTokenizerCreate(kCFAllocatorDefault, string as CFString, inputRange, flag, locale)
var tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer)
while !tokenType.isEmpty {
let currentTokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer)
if currentTokenRange.location <= stringIndex && currentTokenRange.location + currentTokenRange.length > stringIndex {
if currentTokenRange.location <= stringIndex && currentTokenRange.location + currentTokenRange.length > stringIndex {
resultRange = NSRange(location: currentTokenRange.location, length: currentTokenRange.length)
break
}
@ -377,7 +377,7 @@ public final class TextSelectionNode: ASDisplayNode {
}
public func pretendExtendSelection(to index: Int) {
guard let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString, let endRangeRect = cachedLayout.rangeRects(in: NSRange(location: index, length: 1))?.first else {
guard let cachedLayout = self.textNode.cachedLayout, let attributedString = cachedLayout.attributedString, let endRangeRect = cachedLayout.rangeRects(in: NSRange(location: index, length: 1))?.rects.first else {
return
}
let startPoint = self.rightKnob.frame.center
@ -397,15 +397,15 @@ public final class TextSelectionNode: ASDisplayNode {
}
private func updateSelection(range: NSRange?, animateIn: Bool) {
var rects: [CGRect]?
var rects: (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)?
if let range = range {
rects = self.textNode.rangeRects(in: range)
}
self.currentRects = rects
self.currentRects = rects?.rects
if let rects = rects, !rects.isEmpty {
if let (rects, startEdge, endEdge) = rects, !rects.isEmpty {
let highlightOverlay: LinkHighlightingNode
if let current = self.highlightOverlay {
highlightOverlay = current
@ -421,8 +421,8 @@ public final class TextSelectionNode: ASDisplayNode {
highlightOverlay.frame = self.bounds
highlightOverlay.updateRects(rects)
if let image = self.leftKnob.image {
self.leftKnob.frame = CGRect(origin: CGPoint(x: floor(rects[0].minX - 1.0 - image.size.width / 2.0), y: rects[0].minY - 1.0 - self.theme.knobDiameter), size: CGSize(width: image.size.width, height: self.theme.knobDiameter + rects[0].height + 2.0))
self.rightKnob.frame = CGRect(origin: CGPoint(x: floor(rects[rects.count - 1].maxX + 1.0 - image.size.width / 2.0), y: rects[rects.count - 1].maxY + 1.0 - (rects[0].height + 2.0)), size: CGSize(width: image.size.width, height: self.theme.knobDiameter + rects[0].height + 2.0))
self.leftKnob.frame = CGRect(origin: CGPoint(x: floor(startEdge.x - image.size.width / 2.0), y: startEdge.y + 1.0 - 12.0), size: CGSize(width: image.size.width, height: self.theme.knobDiameter + startEdge.height + 2.0))
self.rightKnob.frame = CGRect(origin: CGPoint(x: floor(endEdge.x + 1.0 - image.size.width / 2.0), y: endEdge.y + endEdge.height + 3.0 - (endEdge.height + 2.0)), size: CGSize(width: image.size.width, height: self.theme.knobDiameter + endEdge.height + 2.0))
}
if self.leftKnob.alpha.isZero {
highlightOverlay.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)