Skip to content

Commit

Permalink
Calculates selection rects faster
Browse files Browse the repository at this point in the history
  • Loading branch information
simonbs committed May 4, 2022
1 parent 6fde415 commit 32e8091
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -402,10 +402,6 @@ extension LineController {
return textInputProxy.caretRect(atIndex: index)
}

func selectionRects(in range: NSRange) -> [LineFragmentSelectionRect] {
return textInputProxy.selectionRects(in: range)
}

func firstRect(for range: NSRange) -> CGRect {
return textInputProxy.firstRect(for: range)
}
Expand Down
24 changes: 0 additions & 24 deletions Sources/Runestone/TextView/LineController/LineTextInputProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,30 +21,6 @@ final class LineTextInputProxy {
return CGRect(x: 0, y: yPosition, width: Caret.width, height: estimatedLineFragmentHeight)
}

func selectionRects(in range: NSRange) -> [LineFragmentSelectionRect] {
guard !lineFragments.isEmpty else {
let rect = CGRect(x: 0, y: 0, width: 0, height: estimatedLineFragmentHeight * lineFragmentHeightMultiplier)
return [LineFragmentSelectionRect(rect: rect, range: range, extendsBeyondEnd: false)]
}
var selectionRects: [LineFragmentSelectionRect] = []
for lineFragment in lineFragments {
let line = lineFragment.line
let cfLineRange = CTLineGetStringRange(line)
let lineRange = NSRange(location: cfLineRange.location, length: cfLineRange.length)
let selectionIntersection = range.intersection(lineRange)
if let selectionIntersection = selectionIntersection {
let xStart = CTLineGetOffsetForStringIndex(line, selectionIntersection.lowerBound, nil)
let xEnd = CTLineGetOffsetForStringIndex(line, selectionIntersection.upperBound, nil)
let yPosition = lineFragment.yPosition
let rect = CGRect(x: xStart, y: yPosition, width: xEnd - xStart, height: lineFragment.scaledSize.height)
let extendsBeyondEnd = range.upperBound > selectionIntersection.upperBound
let selectionRect = LineFragmentSelectionRect(rect: rect, range: selectionIntersection, extendsBeyondEnd: extendsBeyondEnd)
selectionRects.append(selectionRect)
}
}
return selectionRects
}

func firstRect(for range: NSRange) -> CGRect {
for lineFragment in lineFragments {
let line = lineFragment.line
Expand Down
66 changes: 31 additions & 35 deletions Sources/Runestone/TextView/TextInput/LayoutManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -487,44 +487,40 @@ extension LayoutManager {
}

func selectionRects(in range: NSRange) -> [TextSelectionRect] {
guard let (startLine, endLine) = lineManager.startAndEndLine(in: range) else {
guard let endLine = lineManager.line(containingCharacterAt: range.upperBound) else {
return []
}
var resultingSelectionRects: [TextSelectionRect] = []
let startLineIndex = startLine.index
var endLineIndex = endLine.index
// If the end line starts where our selection ends then we're only interested in selecting the line break and we will therefore iterate one line less.
if range.upperBound == endLine.location && startLineIndex != endLineIndex {
endLineIndex -= 1
}
let lineIndexRange = startLineIndex ..< endLineIndex + 1
for lineIndex in lineIndexRange {
let line = lineManager.line(atRow: lineIndex)
let lineController = lineController(for: line)
let lineStartLocation = line.location
let lineEndLocation = lineStartLocation + line.data.totalLength
let localRangeLocation = max(range.location, lineStartLocation) - lineStartLocation
let localRangeLength = min(range.location + range.length, lineEndLocation) - lineStartLocation - localRangeLocation
let localRange = NSRange(location: localRangeLocation, length: localRangeLength)
let selectionRects = lineController.selectionRects(in: localRange)
for (selectionRectIdx, selectionRect) in selectionRects.enumerated() {
// Determining containsStart and containsEnd based on indices assumes that the text selection rects are iterated in order.
// This means that `-selectionRects(in:)` on LineController should return them in order.
let containsStart = lineIndex == lineIndexRange.lowerBound && selectionRectIdx == 0
let containsEnd = lineIndex == lineIndexRange.upperBound - 1 && selectionRectIdx == selectionRects.count - 1
let selectionContainsLineBreak = line.data.delimiterLength > 0 && range.contains(lineEndLocation - 1)
var screenRect = selectionRect.rect
screenRect.origin.x += leadingLineSpacing
screenRect.origin.y = textContainerInset.top + line.yPosition + selectionRect.rect.minY
if selectionContainsLineBreak || selectionRect.extendsBeyondEnd {
screenRect.size.width = max(contentWidth, scrollViewWidth) - textContainerInset.right - screenRect.minX
}
resultingSelectionRects += [
TextSelectionRect(rect: screenRect, writingDirection: .natural, containsStart: containsStart, containsEnd: containsEnd)
]
}
let selectsLineEnding = range.upperBound == endLine.location
let adjustedRange = NSRange(location: range.location, length: selectsLineEnding ? range.length - 1 : range.length)
let startCaretRect = caretRect(at: adjustedRange.lowerBound)
let endCaretRect = caretRect(at: adjustedRange.upperBound)
let fullWidth = max(contentWidth, scrollViewWidth) - textContainerInset.right
if startCaretRect.minY == endCaretRect.minY && startCaretRect.maxY == endCaretRect.maxY {
// Selecting text in the same line fragment.
let width = selectsLineEnding ? fullWidth - leadingLineSpacing : endCaretRect.maxX - startCaretRect.maxX
let scaledHeight = startCaretRect.height * lineHeightMultiplier
let offsetY = startCaretRect.minY - (scaledHeight - startCaretRect.height) / 2
let rect = CGRect(x: startCaretRect.minX, y: offsetY, width: width, height: scaledHeight)
let selectionRect = TextSelectionRect(rect: rect, writingDirection: .natural, containsStart: true, containsEnd: true)
return [selectionRect]
} else {
// Selecting text across line fragments and possibly across lines.
let startWidth = fullWidth - startCaretRect.minX
let startScaledHeight = startCaretRect.height * lineHeightMultiplier
let startOffsetY = startCaretRect.minY - (startScaledHeight - startCaretRect.height) / 2
let startRect = CGRect(x: startCaretRect.minX, y: startOffsetY, width: startWidth, height: startScaledHeight)
let endWidth = selectsLineEnding ? fullWidth - leadingLineSpacing : endCaretRect.minX - leadingLineSpacing
let endScaledHeight = endCaretRect.height * lineHeightMultiplier
let endOffsetY = endCaretRect.minY - (endScaledHeight - endCaretRect.height) / 2
let endRect = CGRect(x: leadingLineSpacing, y: endOffsetY, width: endWidth, height: endScaledHeight)
let middleWidth = fullWidth - leadingLineSpacing
let middleHeight = endRect.minY - startRect.maxY
let middleRect = CGRect(x: leadingLineSpacing, y: startRect.maxY, width: middleWidth, height: middleHeight)
let startSelectionRect = TextSelectionRect(rect: startRect, writingDirection: .natural, containsStart: true, containsEnd: false)
let middleSelectionRect = TextSelectionRect(rect: middleRect, writingDirection: .natural, containsStart: false, containsEnd: false)
let endSelectionRect = TextSelectionRect(rect: endRect, writingDirection: .natural, containsStart: false, containsEnd: true)
return [startSelectionRect, middleSelectionRect, endSelectionRect]
}
return resultingSelectionRects.ensuringYAxisAlignment()
}

func closestIndex(to point: CGPoint) -> Int? {
Expand Down
26 changes: 0 additions & 26 deletions Sources/Runestone/TextView/TextInput/TextSelectionRect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,3 @@ final class TextSelectionRect: UITextSelectionRect {
_isVertical = isVertical
}
}

extension Array where Element == TextSelectionRect {
// Ensures that the array of rectangles are all properly aligned on the Y-axis
// so there's no distance between the rectangles and they don't overlap.
func ensuringYAxisAlignment() -> [Element] {
guard count > 1 else {
return self
}
var result: [Element] = [self[0]]
for idx in 1 ..< count {
let previousMaxYPosition = self[idx - 1].rect.maxY
let element = self[idx]
let yPosition = element.rect.minY
let distanceDiff = yPosition - previousMaxYPosition
let newYPosition = yPosition - distanceDiff
let newHeight = element.rect.height + distanceDiff
let newRect = CGRect(x: element.rect.minX, y: newYPosition, width: element.rect.width, height: newHeight)
let newElement = Element(rect: newRect,
writingDirection: element.writingDirection,
containsStart: element.containsStart,
containsEnd: element.containsEnd)
result.append(newElement)
}
return result
}
}

0 comments on commit 32e8091

Please sign in to comment.