Skip to content

Commit 3917f61

Browse files
committed
Add scroll to visible range
- Scrolls to the specified range and centers it
1 parent 6a7a090 commit 3917f61

File tree

2 files changed

+64
-16
lines changed

2 files changed

+64
-16
lines changed

Sources/CodeEditTextView/EmphasizeAPI/EmphasizeAPI.swift

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import AppKit
1111
public class EmphasizeAPI {
1212
// MARK: - Properties
1313

14-
private var highlightedRanges: [EmphasizedRange] = []
15-
private var emphasizedRangeIndex: Int?
14+
public private(set) var emphasizedRanges: [EmphasizedRange] = []
15+
public private(set) var emphasizedRangeIndex: Int?
1616
private let activeColor: NSColor = NSColor(hex: 0xFFFB00, alpha: 1)
1717
private let inactiveColor: NSColor = NSColor.lightGray.withAlphaComponent(0.4)
1818

@@ -23,8 +23,8 @@ public class EmphasizeAPI {
2323
}
2424

2525
// MARK: - Structs
26-
private struct EmphasizedRange {
27-
var range: NSRange
26+
public struct EmphasizedRange {
27+
public var range: NSRange
2828
var layer: CAShapeLayer
2929
}
3030

@@ -61,18 +61,18 @@ public class EmphasizeAPI {
6161
let layer = createEmphasizeLayer(shapePath: shapePath, active: active)
6262
textView?.layer?.insertSublayer(layer, at: 1)
6363

64-
highlightedRanges.append(EmphasizedRange(range: range, layer: layer))
64+
emphasizedRanges.append(EmphasizedRange(range: range, layer: layer))
6565
}
6666

6767
/// Removes the highlight for a specific range.
6868
/// - Parameter range: The range to remove.
6969
public func removeHighlightForRange(_ range: NSRange) {
70-
guard let index = highlightedRanges.firstIndex(where: { $0.range == range }) else { return }
70+
guard let index = emphasizedRanges.firstIndex(where: { $0.range == range }) else { return }
7171

72-
let removedLayer = highlightedRanges[index].layer
72+
let removedLayer = emphasizedRanges[index].layer
7373
removedLayer.removeFromSuperlayer()
7474

75-
highlightedRanges.remove(at: index)
75+
emphasizedRanges.remove(at: index)
7676

7777
// Adjust the active highlight index
7878
if let currentIndex = emphasizedRangeIndex {
@@ -105,8 +105,8 @@ public class EmphasizeAPI {
105105

106106
/// Removes all emphasised ranges.
107107
public func removeEmphasizeLayers() {
108-
highlightedRanges.forEach { $0.layer.removeFromSuperlayer() }
109-
highlightedRanges.removeAll()
108+
emphasizedRanges.forEach { $0.layer.removeFromSuperlayer() }
109+
emphasizedRanges.removeAll()
110110
emphasizedRangeIndex = nil
111111
}
112112

@@ -147,29 +147,29 @@ public class EmphasizeAPI {
147147
/// - Returns: An optional `NSRange` representing the newly active highlight, colored in the active color.
148148
/// Returns `nil` if no change occurred (e.g., if there are no highlighted ranges).
149149
private func shiftActiveHighlight(amount: Int) -> NSRange? {
150-
guard !highlightedRanges.isEmpty else { return nil }
150+
guard !emphasizedRanges.isEmpty else { return nil }
151151

152152
var currentIndex = emphasizedRangeIndex ?? -1
153-
currentIndex = (currentIndex + amount + highlightedRanges.count) % highlightedRanges.count
153+
currentIndex = (currentIndex + amount + emphasizedRanges.count) % emphasizedRanges.count
154154

155-
guard currentIndex < highlightedRanges.count else { return nil }
155+
guard currentIndex < emphasizedRanges.count else { return nil }
156156

157157
// Reset the previously active layer
158158
if let currentIndex = emphasizedRangeIndex {
159-
let previousLayer = highlightedRanges[currentIndex].layer
159+
let previousLayer = emphasizedRanges[currentIndex].layer
160160
previousLayer.fillColor = inactiveColor.cgColor
161161
previousLayer.shadowOpacity = 0.0
162162
}
163163

164164
// Set the new active layer
165-
let newLayer = highlightedRanges[currentIndex].layer
165+
let newLayer = emphasizedRanges[currentIndex].layer
166166
newLayer.fillColor = activeColor.cgColor
167167
newLayer.shadowOpacity = 0.3
168168

169169
applyPopAnimation(to: newLayer)
170170
emphasizedRangeIndex = currentIndex
171171

172-
return highlightedRanges[currentIndex].range
172+
return emphasizedRanges[currentIndex].range
173173
}
174174

175175
private func applyPopAnimation(to layer: CALayer) {

Sources/CodeEditTextView/TextView/TextView+ScrollToVisible.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import AppKit
910

1011
extension TextView {
1112
fileprivate typealias Direction = TextSelectionManager.Direction
@@ -36,6 +37,53 @@ extension TextView {
3637
}
3738
}
3839

40+
public func scrollToRange(_ range: NSRange) {
41+
guard let scrollView else { return }
42+
43+
guard let boundingRect = layoutManager.rectForOffset(range.location) else { return }
44+
45+
// Check if the range is already visible
46+
if visibleRect.contains(boundingRect) {
47+
return // No scrolling needed
48+
}
49+
50+
// Calculate the target offset to center the range in the view
51+
let targetOffset = CGPoint(
52+
x: max(boundingRect.midX - visibleRect.width / 2, 0),
53+
y: max(boundingRect.midY - visibleRect.height / 2, 0)
54+
)
55+
56+
var lastFrame: CGRect = .zero
57+
58+
// Set a timeout to avoid a infinite loop
59+
let timeout: TimeInterval = 0.5
60+
let startTime = Date()
61+
62+
// Adjust layout until stable
63+
while let newRect = layoutManager.rectForOffset(range.location),
64+
lastFrame != newRect,
65+
Date().timeIntervalSince(startTime) < timeout {
66+
lastFrame = newRect
67+
layoutManager.layoutLines()
68+
selectionManager.updateSelectionViews()
69+
selectionManager.drawSelections(in: visibleRect)
70+
}
71+
72+
// Scroll to make the range appear in the middle of the screen
73+
if lastFrame != .zero {
74+
let animated = false // feature flag
75+
if animated {
76+
NSAnimationContext.runAnimationGroup { context in
77+
context.duration = 0.15 // Adjust duration as needed
78+
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
79+
scrollView.contentView.animator().setBoundsOrigin(targetOffset)
80+
}
81+
} else {
82+
scrollView.contentView.scroll(to: targetOffset)
83+
}
84+
}
85+
}
86+
3987
/// Get the selection that should be scrolled to visible for the current text selection.
4088
/// - Returns: The the selection to scroll to.
4189
private func getSelection() -> TextSelection? {

0 commit comments

Comments
 (0)