Skip to content

Commit

Permalink
Finalize scrollSelectionToVisible Logic
Browse files Browse the repository at this point in the history
  • Loading branch information
thecoolwinter committed Jun 15, 2024
1 parent b51f8e2 commit daa888d
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 69 deletions.
54 changes: 27 additions & 27 deletions Sources/CodeEditTextView/TextView/TextView+Move.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,155 +8,155 @@
import Foundation

extension TextView {
fileprivate func updateAfterMove(direction: TextSelectionManager.Direction) {
fileprivate func updateAfterMove() {
unmarkTextIfNeeded()
scrollSelectionToVisible()
}

/// Moves the cursors up one character.
override public func moveUp(_ sender: Any?) {
selectionManager.moveSelections(direction: .up, destination: .character)
updateAfterMove(direction: .up)
updateAfterMove()
}

/// Moves the cursors up one character extending the current selection.
override public func moveUpAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .up, destination: .character, modifySelection: true)
updateAfterMove(direction: .up)
updateAfterMove()
}

/// Moves the cursors down one character.
override public func moveDown(_ sender: Any?) {
selectionManager.moveSelections(direction: .down, destination: .character)
updateAfterMove(direction: .down)
updateAfterMove()
}

/// Moves the cursors down one character extending the current selection.
override public func moveDownAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .down, destination: .character, modifySelection: true)
updateAfterMove(direction: .down)
updateAfterMove()
}

/// Moves the cursors left one character.
override public func moveLeft(_ sender: Any?) {
selectionManager.moveSelections(direction: .backward, destination: .character)
updateAfterMove(direction: .backward)
updateAfterMove()
}

/// Moves the cursors left one character extending the current selection.
override public func moveLeftAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .backward, destination: .character, modifySelection: true)
updateAfterMove(direction: .backward)
updateAfterMove()
}

/// Moves the cursors right one character.
override public func moveRight(_ sender: Any?) {
selectionManager.moveSelections(direction: .forward, destination: .character)
updateAfterMove(direction: .forward)
updateAfterMove()
}

/// Moves the cursors right one character extending the current selection.
override public func moveRightAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .forward, destination: .character, modifySelection: true)
updateAfterMove(direction: .forward)
updateAfterMove()
}

/// Moves the cursors left one word.
override public func moveWordLeft(_ sender: Any?) {
selectionManager.moveSelections(direction: .backward, destination: .word)
updateAfterMove(direction: .backward)
updateAfterMove()
}

/// Moves the cursors left one word extending the current selection.
override public func moveWordLeftAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .backward, destination: .word, modifySelection: true)
updateAfterMove(direction: .backward)
updateAfterMove()
}

/// Moves the cursors right one word.
override public func moveWordRight(_ sender: Any?) {
selectionManager.moveSelections(direction: .forward, destination: .word)
updateAfterMove(direction: .forward)
updateAfterMove()
}

/// Moves the cursors right one word extending the current selection.
override public func moveWordRightAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .forward, destination: .word, modifySelection: true)
updateAfterMove(direction: .forward)
updateAfterMove()
}

/// Moves the cursors left to the end of the line.
override public func moveToLeftEndOfLine(_ sender: Any?) {
selectionManager.moveSelections(direction: .backward, destination: .visualLine)
updateAfterMove(direction: .backward)
updateAfterMove()
}

/// Moves the cursors left to the end of the line extending the current selection.
override public func moveToLeftEndOfLineAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .backward, destination: .visualLine, modifySelection: true)
updateAfterMove(direction: .backward)
updateAfterMove()
}

/// Moves the cursors right to the end of the line.
override public func moveToRightEndOfLine(_ sender: Any?) {
selectionManager.moveSelections(direction: .forward, destination: .visualLine)
updateAfterMove(direction: .forward)
updateAfterMove()
}

/// Moves the cursors right to the end of the line extending the current selection.
override public func moveToRightEndOfLineAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .forward, destination: .visualLine, modifySelection: true)
updateAfterMove(direction: .forward)
updateAfterMove()
}

/// Moves the cursors to the beginning of the line, if pressed again selects the next line up.
override public func moveToBeginningOfParagraph(_ sender: Any?) {
selectionManager.moveSelections(direction: .up, destination: .line)
updateAfterMove(direction: .up)
updateAfterMove()
}

/// Moves the cursors to the beginning of the line, if pressed again selects the next line up extending the current
/// selection.
override public func moveToBeginningOfParagraphAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .up, destination: .line, modifySelection: true)
updateAfterMove(direction: .up)
updateAfterMove()
}

/// Moves the cursors to the end of the line, if pressed again selects the next line up.
override public func moveToEndOfParagraph(_ sender: Any?) {
selectionManager.moveSelections(direction: .down, destination: .line)
updateAfterMove(direction: .down)
updateAfterMove()
}

/// Moves the cursors to the end of the line, if pressed again selects the next line up extending the current
/// selection.
override public func moveToEndOfParagraphAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .down, destination: .line, modifySelection: true)
updateAfterMove(direction: .down)
updateAfterMove()
}

/// Moves the cursors to the beginning of the document.
override public func moveToBeginningOfDocument(_ sender: Any?) {
selectionManager.moveSelections(direction: .up, destination: .document)
updateAfterMove(direction: .up)
updateAfterMove()
}

/// Moves the cursors to the beginning of the document extending the current selection.
override public func moveToBeginningOfDocumentAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .up, destination: .document, modifySelection: true)
updateAfterMove(direction: .up)
updateAfterMove()
}

/// Moves the cursors to the end of the document.
override public func moveToEndOfDocument(_ sender: Any?) {
selectionManager.moveSelections(direction: .down, destination: .document)
updateAfterMove(direction: .down)
updateAfterMove()
}

/// Moves the cursors to the end of the document extending the current selection.
override public func moveToEndOfDocumentAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .down, destination: .document, modifySelection: true)
updateAfterMove(direction: .down)
updateAfterMove()
}

override public func pageUp(_ sender: Any?) {
Expand All @@ -165,7 +165,7 @@ extension TextView {

override public func pageUpAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .up, destination: .page, modifySelection: true)
updateAfterMove(direction: .up)
updateAfterMove()
}

override public func pageDown(_ sender: Any?) {
Expand All @@ -174,6 +174,6 @@ extension TextView {

override public func pageDownAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .down, destination: .page, modifySelection: true)
updateAfterMove(direction: .down)
updateAfterMove()
}
}
70 changes: 28 additions & 42 deletions Sources/CodeEditTextView/TextView/TextView+ScrollToVisible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,24 @@
import Foundation

extension TextView {
fileprivate typealias Direction = TextSelectionManager.Direction
fileprivate typealias TextSelection = TextSelectionManager.TextSelection

/// Scrolls the upmost selection to the visible rect if `scrollView` is not `nil`.
/// - Parameter updateDirection: (optional) the direction of a change in selection. Used to try and keep
/// contextual portions of the selection in the viewport.
public func scrollSelectionToVisible(updateDirection: TextSelectionManager.Direction? = nil) {
guard let scrollView else {
public func scrollSelectionToVisible() {
guard let scrollView, let selection = getSelection() else {
return
}

let offsetToScrollTo = offsetNotPivot(selection)

// There's a bit of a chicken-and-the-egg issue going on here. We need to know the rect to scroll to, but we
// can't know the exact rect to make visible without laying out the text. Then, once text is laid out the
// selection rect may be different again. To solve this, we loop until the frame doesn't change after a layout
// pass and scroll to that rect.

var lastFrame: CGRect = .zero
while let boundingRect = getSelectionRect(updateDirection),
lastFrame != boundingRect {
while let boundingRect = layoutManager.rectForOffset(offsetToScrollTo), lastFrame != boundingRect {
lastFrame = boundingRect
layoutManager.layoutLines()
selectionManager.updateSelectionViews()
Expand All @@ -34,42 +36,26 @@ extension TextView {
}
}

/// Get the rect that should be scrolled to visible for the current text selection.
/// - Parameter updateDirection: The direction of the update.
/// - Returns: The rect of the selection.
private func getSelectionRect(_ updateDirection: TextSelectionManager.Direction?) -> CGRect? {
switch updateDirection {
case .forward, .backward, nil:
return selectionManager
.textSelections
.sorted(by: { $0.boundingRect.origin.y < $1.boundingRect.origin.y })
.first?
.boundingRect
case .up:
guard let selection = selectionManager
.textSelections
.sorted(by: { $0.range.location < $1.range.location }) // Get the highest one.
.first,
let minRect = layoutManager.rectForOffset(selection.range.location) else {
return nil
}
return CGRect(
origin: minRect.origin,
size: CGSize(width: selection.boundingRect.width, height: layoutManager.estimateLineHeight())
)
case .down:
guard let selection = selectionManager
.textSelections
.sorted(by: { $0.range.max > $1.range.max }) // Get the lowest one.
.first,
let maxRect = layoutManager.rectForOffset(selection.range.max) else {
return nil
}
let lineHeight = layoutManager.estimateLineHeight()
return CGRect(
origin: CGPoint(x: selection.boundingRect.origin.x, y: maxRect.maxY - lineHeight),
size: CGSize(width: selection.boundingRect.width, height: lineHeight)
)
/// Get the selection that should be scrolled to visible for the current text selection.
/// - Returns: The the selection to scroll to.
private func getSelection() -> TextSelection? {
selectionManager
.textSelections
.sorted(by: { $0.range.max > $1.range.max }) // Get the lowest one.
.first
}

/// Returns the offset that isn't the pivot of the selection.
/// - Parameter selection: The selection to use.
/// - Returns: The offset suitable for scrolling to.
private func offsetNotPivot(_ selection: TextSelection) -> Int {
guard let pivot = selection.pivot else {
return selection.range.location
}
if selection.range.location == pivot {
return selection.range.max
} else {
return selection.range.location
}
}
}

0 comments on commit daa888d

Please sign in to comment.