Skip to content

Commit a8928ed

Browse files
committed
Start on Scrolling Offset
1 parent 39867e9 commit a8928ed

File tree

5 files changed

+128
-29
lines changed

5 files changed

+128
-29
lines changed

Package.resolved

Lines changed: 0 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// MinimapContentView.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 4/11/25.
6+
//
7+
8+
import AppKit
9+
10+
final class MinimapContentView: FlippedNSView {
11+
override func draw(_ dirtyRect: NSRect) {
12+
guard let context = NSGraphicsContext.current?.cgContext else { return }
13+
context.saveGState()
14+
15+
context.setFillColor(NSColor.separatorColor.cgColor)
16+
context.fill([
17+
CGRect(x: 0, y: 0, width: 1, height: frame.height)
18+
])
19+
20+
context.restoreGState()
21+
}
22+
}

Sources/CodeEditSourceEditor/Minimap/MinimapLineRenderer.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,22 @@ final class MinimapLineRenderer: TextLayoutManagerRenderDelegate {
4040
// Make all fragments 2px tall
4141
textLine.lineFragments.forEach { fragmentPosition in
4242
let remainingHeight = fragmentPosition.height - 3.0
43-
textLine.lineFragments.update(
44-
atOffset: fragmentPosition.range.location,
45-
delta: 0,
46-
deltaHeight: -remainingHeight
47-
)
43+
if remainingHeight != 0 {
44+
textLine.lineFragments.update(
45+
atOffset: fragmentPosition.range.location,
46+
delta: 0,
47+
deltaHeight: -remainingHeight
48+
)
49+
}
4850
fragmentPosition.data.height = 2.0
4951
fragmentPosition.data.scaledHeight = 3.0
5052
}
5153
}
5254

55+
func estimatedLineHeight() -> CGFloat? {
56+
3.0
57+
}
58+
5359
func lineFragmentView(for lineFragment: LineFragment) -> LineFragmentView {
5460
MinimapLineFragmentView(textStorage: textView?.textStorage)
5561
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// MinimapView+DocumentVisibleView.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 4/11/25.
6+
//
7+
8+
import AppKit
9+
10+
extension MinimapView {
11+
func updateDocumentVisibleViewPosition() {
12+
guard let textView = textView, let editorScrollView = textView.enclosingScrollView else { return }
13+
layoutManager?.layoutLines(in: scrollView.documentVisibleRect)
14+
let editorHeight = textView.frame.height
15+
let minimapHeight = contentView.frame.height
16+
17+
let containerHeight = scrollView.documentVisibleRect.height
18+
let scrollPercentage = (
19+
editorScrollView.documentVisibleRect.origin.y + editorScrollView.contentInsets.top
20+
) / textView.frame.height
21+
// let scrollOffset = editorScrollView.documentVisibleRect.origin.y
22+
23+
// let scrollMultiplier: CGFloat = if minimapHeight < containerHeight {
24+
// 1.0
25+
// } else {
26+
// 1.0 - (minimapHeight - containerHeight) / (editorHeight - containerHeight)
27+
// }
28+
29+
let newMinimapOrigin = minimapHeight * scrollPercentage
30+
scrollView.contentView.bounds.origin.y = newMinimapOrigin - editorScrollView.contentInsets.top
31+
}
32+
}

Sources/CodeEditSourceEditor/Minimap/MinimapView.swift

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,40 @@ import CodeEditTextView
1111
class MinimapView: NSView {
1212
weak var textView: TextView?
1313

14+
/// The container scrollview for the minimap contents.
1415
let scrollView: NSScrollView
15-
let contentView: FlippedNSView
16+
/// The view text lines are rendered into.
17+
let contentView: MinimapContentView
18+
/// The box displaying the visible region on the minimap.
19+
let documentVisibleView: NSView
20+
21+
/// The layout manager that uses the ``lineRenderer`` to render and layout lines.
1622
var layoutManager: TextLayoutManager?
23+
/// A custom line renderer that lays out lines of text as 2px tall and draws contents as small lines
24+
/// using ``MinimapLineFragmentView``
1725
let lineRenderer: MinimapLineRenderer
1826

1927
var theme: EditorTheme {
2028
didSet {
29+
documentVisibleView.layer?.backgroundColor = theme.text.color.withAlphaComponent(0.1).cgColor
2130
layer?.backgroundColor = theme.background.cgColor
2231
}
2332
}
2433

34+
var visibleTextRange: NSRange? {
35+
guard let layoutManager = layoutManager else { return nil }
36+
let minY = max(visibleRect.minY, 0)
37+
let maxY = min(visibleRect.maxY, layoutManager.estimatedHeight())
38+
guard let minYLine = layoutManager.textLineForPosition(minY),
39+
let maxYLine = layoutManager.textLineForPosition(maxY) else {
40+
return nil
41+
}
42+
return NSRange(
43+
location: minYLine.range.location,
44+
length: (maxYLine.range.location - minYLine.range.location) + maxYLine.range.length
45+
)
46+
}
47+
2548
init(textView: TextView, theme: EditorTheme) {
2649
self.textView = textView
2750
self.theme = theme
@@ -34,12 +57,18 @@ class MinimapView: NSView {
3457
scrollView.drawsBackground = false
3558
scrollView.verticalScrollElasticity = .none
3659

37-
self.contentView = FlippedNSView(frame: .zero)
60+
self.contentView = MinimapContentView(frame: .zero)
3861
contentView.translatesAutoresizingMaskIntoConstraints = false
3962

63+
self.documentVisibleView = NSView()
64+
documentVisibleView.translatesAutoresizingMaskIntoConstraints = false
65+
documentVisibleView.wantsLayer = true
66+
documentVisibleView.layer?.backgroundColor = theme.text.color.withAlphaComponent(0.1).cgColor
67+
4068
super.init(frame: .zero)
4169

4270
addSubview(scrollView)
71+
addSubview(documentVisibleView)
4372
scrollView.documentView = contentView
4473

4574
self.translatesAutoresizingMaskIntoConstraints = false
@@ -58,6 +87,7 @@ class MinimapView: NSView {
5887
layer?.backgroundColor = theme.background.cgColor
5988

6089
setUpConstraints()
90+
setUpListeners()
6191
}
6292

6393
private func setUpConstraints() {
@@ -67,10 +97,40 @@ class MinimapView: NSView {
6797
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
6898
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
6999

70-
contentView.widthAnchor.constraint(equalTo: widthAnchor)
100+
contentView.widthAnchor.constraint(equalTo: widthAnchor),
101+
102+
documentVisibleView.leadingAnchor.constraint(equalTo: leadingAnchor),
103+
documentVisibleView.trailingAnchor.constraint(equalTo: trailingAnchor)
71104
])
72105
}
73106

107+
private func setUpListeners() {
108+
guard let editorScrollView = textView?.enclosingScrollView else { return }
109+
// Need to listen to:
110+
// - ScrollView offset changed
111+
// - ScrollView frame changed
112+
// and update the document visible box to match.
113+
NotificationCenter.default.addObserver(
114+
forName: NSView.boundsDidChangeNotification,
115+
object: editorScrollView.contentView,
116+
queue: .main
117+
) { [weak self] _ in
118+
// Scroll changed
119+
self?.layoutManager?.layoutLines()
120+
self?.updateDocumentVisibleViewPosition()
121+
}
122+
123+
NotificationCenter.default.addObserver(
124+
forName: NSView.frameDidChangeNotification,
125+
object: editorScrollView.contentView,
126+
queue: .main
127+
) { [weak self] _ in
128+
// Frame changed
129+
self?.layoutManager?.layoutLines()
130+
self?.updateDocumentVisibleViewPosition()
131+
}
132+
}
133+
74134
required init?(coder: NSCoder) {
75135
fatalError("init(coder:) has not been implemented")
76136
}
@@ -86,18 +146,6 @@ class MinimapView: NSView {
86146
super.layout()
87147
}
88148

89-
override func draw(_ dirtyRect: NSRect) {
90-
guard let context = NSGraphicsContext.current?.cgContext else { return }
91-
context.saveGState()
92-
93-
context.setFillColor(NSColor.separatorColor.cgColor)
94-
context.fill([
95-
CGRect(x: 0, y: 0, width: 1, height: frame.height)
96-
])
97-
98-
context.restoreGState()
99-
}
100-
101149
override func hitTest(_ point: NSPoint) -> NSView? {
102150
if visibleRect.contains(point) {
103151
return self

0 commit comments

Comments
 (0)