Skip to content

Commit 17035f6

Browse files
committed
Correctly Wrap Lines, Render Line Breaks, Add to ScrollView
1 parent cb422bb commit 17035f6

File tree

7 files changed

+118
-31
lines changed

7 files changed

+118
-31
lines changed

Sources/CodeEditSourceEditor/Controller/EditorContainerView.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,31 @@ class EditorContainerView: NSView {
2222
addSubview(scrollView)
2323
addSubview(minimapView)
2424

25+
scrollView.hasVerticalScroller = true
26+
27+
let maxWidthConstraint = minimapView.widthAnchor.constraint(lessThanOrEqualToConstant: 150)
28+
let relativeWidthConstraint = minimapView.widthAnchor.constraint(
29+
equalTo: widthAnchor,
30+
multiplier: 0.18
31+
)
32+
relativeWidthConstraint.priority = .defaultLow
33+
34+
guard let scrollerAnchor = scrollView.verticalScroller?.leadingAnchor else {
35+
assertionFailure("Scroll view failed to create a scroller.")
36+
return
37+
}
38+
2539
NSLayoutConstraint.activate([
2640
scrollView.topAnchor.constraint(equalTo: topAnchor),
2741
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
2842
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
2943
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
3044

31-
minimapView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
45+
minimapView.topAnchor.constraint(equalTo: topAnchor),
3246
minimapView.bottomAnchor.constraint(equalTo: bottomAnchor),
33-
minimapView.trailingAnchor.constraint(equalTo: trailingAnchor),
34-
minimapView.widthAnchor.constraint(equalToConstant: 150)
47+
minimapView.trailingAnchor.constraint(equalTo: scrollerAnchor),
48+
maxWidthConstraint,
49+
relativeWidthConstraint
3550
])
3651
}
3752

Sources/CodeEditSourceEditor/Controller/TextViewController+StyleViews.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,19 @@ extension TextViewController {
7171
if let contentInsets {
7272
scrollView.automaticallyAdjustsContentInsets = false
7373
scrollView.contentInsets = contentInsets
74+
minimapView.scrollView.contentInsets.top = contentInsets.top
75+
minimapView.scrollView.contentInsets.top = contentInsets.bottom
7476
} else {
7577
scrollView.automaticallyAdjustsContentInsets = true
78+
minimapView.scrollView.automaticallyAdjustsContentInsets = true
7679
}
7780

7881
scrollView.contentInsets.top += additionalTextInsets?.top ?? 0
7982
scrollView.contentInsets.bottom += additionalTextInsets?.bottom ?? 0
8083

8184
scrollView.contentInsets.top += (findViewController?.isShowingFindPanel ?? false) ? FindPanel.height : 0
85+
minimapView.scrollView.contentInsets.top += (
86+
findViewController?.isShowingFindPanel ?? false
87+
) ? FindPanel.height : 0
8288
}
8389
}

Sources/CodeEditSourceEditor/Minimap/MinimapLineFragmentView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ final class MinimapLineFragmentView: LineFragmentView {
113113
context.saveGState()
114114
for run in drawingRuns {
115115
let rect = CGRect(
116-
x: 8 + (CGFloat(run.range.location) * 2),
116+
x: 8 + (CGFloat(run.range.location) * 1.5),
117117
y: 0,
118-
width: CGFloat(run.range.length) * 2,
118+
width: CGFloat(run.range.length) * 1.5,
119119
height: 2.0
120120
)
121121
context.setFillColor(run.color.cgColor)

Sources/CodeEditSourceEditor/Minimap/MinimapLineRenderer.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class MinimapLineRenderer: TextLayoutManagerRenderDelegate {
2424
breakStrategy: LineBreakStrategy
2525
) {
2626
let maxWidth: CGFloat = if let textView, textView.wrapLines {
27-
textView.frame.width
27+
textView.layoutManager.maxLineLayoutWidth
2828
} else {
2929
.infinity
3030
}
@@ -39,10 +39,11 @@ final class MinimapLineRenderer: TextLayoutManagerRenderDelegate {
3939

4040
// Make all fragments 2px tall
4141
textLine.lineFragments.forEach { fragmentPosition in
42+
let remainingHeight = fragmentPosition.height - 3.0
4243
textLine.lineFragments.update(
43-
atIndex: fragmentPosition.index,
44+
atOffset: fragmentPosition.range.location,
4445
delta: 0,
45-
deltaHeight: -(fragmentPosition.height - 3.0)
46+
deltaHeight: -remainingHeight
4647
)
4748
fragmentPosition.data.height = 2.0
4849
fragmentPosition.data.scaledHeight = 3.0
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// MinimapView+TextLayoutManagerDelegate.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 4/11/25.
6+
//
7+
8+
import AppKit
9+
import CodeEditTextView
10+
11+
extension MinimapView: TextLayoutManagerDelegate {
12+
func layoutManagerHeightDidUpdate(newHeight: CGFloat) {
13+
contentView.frame.size.height = newHeight
14+
}
15+
16+
func layoutManagerMaxWidthDidChange(newWidth: CGFloat) { }
17+
18+
func layoutManagerTypingAttributes() -> [NSAttributedString.Key: Any] {
19+
textView?.layoutManagerTypingAttributes() ?? [:]
20+
}
21+
22+
func textViewportSize() -> CGSize {
23+
var size = scrollView.contentSize
24+
size.height -= scrollView.contentInsets.top + scrollView.contentInsets.bottom
25+
size.width = textView?.layoutManager.maxLineLayoutWidth ?? size.width
26+
return size
27+
}
28+
29+
func layoutManagerYAdjustment(_ yAdjustment: CGFloat) {
30+
var point = scrollView.documentVisibleRect.origin
31+
point.y += yAdjustment
32+
scrollView.documentView?.scroll(point)
33+
}
34+
}

Sources/CodeEditSourceEditor/Minimap/MinimapView.swift

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import CodeEditTextView
1010

1111
class MinimapView: NSView {
1212
weak var textView: TextView?
13+
14+
let scrollView: NSScrollView
15+
let contentView: FlippedNSView
1316
var layoutManager: TextLayoutManager?
1417
let lineRenderer: MinimapLineRenderer
1518

@@ -19,21 +22,32 @@ class MinimapView: NSView {
1922
}
2023
}
2124

22-
override var isFlipped: Bool { true }
23-
2425
init(textView: TextView, theme: EditorTheme) {
2526
self.textView = textView
2627
self.theme = theme
2728
self.lineRenderer = MinimapLineRenderer(textView: textView)
2829

30+
self.scrollView = NSScrollView()
31+
scrollView.translatesAutoresizingMaskIntoConstraints = false
32+
scrollView.hasVerticalScroller = false
33+
scrollView.hasHorizontalScroller = false
34+
scrollView.drawsBackground = false
35+
scrollView.verticalScrollElasticity = .none
36+
37+
self.contentView = FlippedNSView(frame: .zero)
38+
contentView.translatesAutoresizingMaskIntoConstraints = false
39+
2940
super.init(frame: .zero)
3041

42+
addSubview(scrollView)
43+
scrollView.documentView = contentView
44+
3145
self.translatesAutoresizingMaskIntoConstraints = false
3246
let layoutManager = TextLayoutManager(
3347
textStorage: textView.textStorage,
3448
lineHeightMultiplier: 1.0,
3549
wrapLines: textView.wrapLines,
36-
textView: self,
50+
textView: contentView,
3751
delegate: self,
3852
renderDelegate: lineRenderer
3953
)
@@ -42,12 +56,31 @@ class MinimapView: NSView {
4256

4357
wantsLayer = true
4458
layer?.backgroundColor = theme.background.cgColor
59+
60+
setUpConstraints()
61+
}
62+
63+
private func setUpConstraints() {
64+
NSLayoutConstraint.activate([
65+
scrollView.topAnchor.constraint(equalTo: topAnchor),
66+
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
67+
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
68+
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
69+
70+
contentView.widthAnchor.constraint(equalTo: widthAnchor)
71+
])
4572
}
4673

4774
required init?(coder: NSCoder) {
4875
fatalError("init(coder:) has not been implemented")
4976
}
5077

78+
override public var visibleRect: NSRect {
79+
var rect = scrollView.documentVisibleRect
80+
rect.origin.y += scrollView.contentInsets.top
81+
return rect.pixelAligned
82+
}
83+
5184
override func layout() {
5285
layoutManager?.layoutLines()
5386
super.layout()
@@ -64,26 +97,12 @@ class MinimapView: NSView {
6497

6598
context.restoreGState()
6699
}
67-
}
68-
69-
extension MinimapView: TextLayoutManagerDelegate {
70-
func layoutManagerHeightDidUpdate(newHeight: CGFloat) {
71-
72-
}
73-
74-
func layoutManagerMaxWidthDidChange(newWidth: CGFloat) {
75-
76-
}
77-
78-
func layoutManagerTypingAttributes() -> [NSAttributedString.Key: Any] {
79-
textView?.layoutManagerTypingAttributes() ?? [:]
80-
}
81100

82-
func textViewportSize() -> CGSize {
83-
self.frame.size
84-
}
85-
86-
func layoutManagerYAdjustment(_ yAdjustment: CGFloat) {
87-
// TODO: Adjust things
101+
override func hitTest(_ point: NSPoint) -> NSView? {
102+
if visibleRect.contains(point) {
103+
return self
104+
} else {
105+
return super.hitTest(point)
106+
}
88107
}
89108
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// FlippedNSView.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 4/11/25.
6+
//
7+
8+
import AppKit
9+
10+
class FlippedNSView: NSView {
11+
override var isFlipped: Bool { true }
12+
}

0 commit comments

Comments
 (0)