Skip to content

Commit b5c4f8a

Browse files
committed
Draw Selections
1 parent b7f341f commit b5c4f8a

File tree

6 files changed

+79
-6
lines changed

6 files changed

+79
-6
lines changed

Sources/CodeEditSourceEditor/Minimap/MinimapLineFragmentView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import CodeEditTextView
1010

1111
/// A custom line fragment view for the minimap.
1212
///
13-
/// Instead of drawing line contents, this view calculates a series of boxes or 'runs' to draw to represent the text
13+
/// Instead of drawing line contents, this view calculates a series of bubbles or 'runs' to draw to represent the text
1414
/// in the line fragment.
1515
///
1616
/// Runs are calculated when the view's fragment is set, and cached until invalidated, and all whitespace
@@ -114,12 +114,12 @@ final class MinimapLineFragmentView: LineFragmentView {
114114
for run in drawingRuns {
115115
let rect = CGRect(
116116
x: 8 + (CGFloat(run.range.location) * 1.5),
117-
y: 0,
117+
y: 0.25,
118118
width: CGFloat(run.range.length) * 1.5,
119119
height: 2.0
120120
)
121121
context.setFillColor(run.color.cgColor)
122-
context.fill(rect)
122+
context.fill(rect.pixelAligned)
123123
}
124124

125125
context.restoreGState()

Sources/CodeEditSourceEditor/Minimap/MinimapLineRenderer.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,8 @@ final class MinimapLineRenderer: TextLayoutManagerRenderDelegate {
5959
func lineFragmentView(for lineFragment: LineFragment) -> LineFragmentView {
6060
MinimapLineFragmentView(textStorage: textView?.textStorage)
6161
}
62+
63+
func characterXPosition(in lineFragment: LineFragment, for offset: Int) -> CGFloat {
64+
8 + (CGFloat(offset) * 1.5)
65+
}
6266
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// MinimapView+Draw.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 4/16/25.
6+
//
7+
8+
import AppKit
9+
import CodeEditTextView
10+
11+
public class MinimapContentView: FlippedNSView {
12+
weak var textView: TextView?
13+
weak var selectionManager: TextSelectionManager?
14+
15+
override public func draw(_ dirtyRect: NSRect) {
16+
super.draw(dirtyRect)
17+
if textView?.isSelectable ?? false {
18+
selectionManager?.drawSelections(in: dirtyRect)
19+
}
20+
}
21+
}

Sources/CodeEditSourceEditor/Minimap/MinimapView+TextLayoutManagerDelegate.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import CodeEditTextView
1010

1111
extension MinimapView: TextLayoutManagerDelegate {
1212
public func layoutManagerHeightDidUpdate(newHeight: CGFloat) {
13-
// contentView.frame.size.height = newHeight
1413
updateContentViewHeight()
1514
}
1615

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// MinimapView+TextSelectionManagerDelegate.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 4/16/25.
6+
//
7+
8+
import AppKit
9+
import CodeEditTextView
10+
11+
extension MinimapView: TextSelectionManagerDelegate {
12+
public var visibleTextRange: NSRange? {
13+
let minY = max(visibleRect.minY, 0)
14+
let maxY = min(visibleRect.maxY, layoutManager?.estimatedHeight() ?? 3.0)
15+
guard let minYLine = layoutManager?.textLineForPosition(minY),
16+
let maxYLine = layoutManager?.textLineForPosition(maxY) else {
17+
return nil
18+
}
19+
return NSRange(start: minYLine.range.location, end: maxYLine.range.max)
20+
}
21+
22+
public func setNeedsDisplay() {
23+
contentView.needsDisplay = true
24+
}
25+
26+
public func estimatedLineHeight() -> CGFloat {
27+
layoutManager?.estimateLineHeight() ?? 3.0
28+
}
29+
}

Sources/CodeEditSourceEditor/Minimap/MinimapView.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class MinimapView: FlippedNSView {
3030
/// The container scrollview for the minimap contents.
3131
public let scrollView: ForwardingScrollView
3232
/// The view text lines are rendered into.
33-
public let contentView: FlippedNSView
33+
public let contentView: MinimapContentView
3434
/// The box displaying the visible region on the minimap.
3535
public let documentVisibleView: NSView
3636
/// A small gray line on the left of the minimap distinguishing it from the editor.
@@ -41,6 +41,7 @@ public class MinimapView: FlippedNSView {
4141

4242
/// The layout manager that uses the ``lineRenderer`` to render and layout lines.
4343
var layoutManager: TextLayoutManager?
44+
var selectionManager: TextSelectionManager?
4445
/// A custom line renderer that lays out lines of text as 2px tall and draws contents as small lines
4546
/// using ``MinimapLineFragmentView``
4647
let lineRenderer: MinimapLineRenderer
@@ -82,7 +83,7 @@ public class MinimapView: FlippedNSView {
8283
scrollView.verticalScrollElasticity = .none
8384
scrollView.receiver = textView.enclosingScrollView
8485

85-
self.contentView = FlippedNSView()
86+
self.contentView = MinimapContentView()
8687
contentView.translatesAutoresizingMaskIntoConstraints = false
8788

8889
self.documentVisibleView = NSView()
@@ -121,6 +122,15 @@ public class MinimapView: FlippedNSView {
121122
self.layoutManager = layoutManager
122123
(textView.textStorage.delegate as? MultiStorageDelegate)?.addDelegate(layoutManager)
123124

125+
self.selectionManager = TextSelectionManager(
126+
layoutManager: layoutManager,
127+
textStorage: textView.textStorage,
128+
textView: textView,
129+
delegate: self
130+
)
131+
contentView.textView = textView
132+
contentView.selectionManager = selectionManager
133+
124134
wantsLayer = true
125135
layer?.backgroundColor = theme.background.cgColor
126136

@@ -180,6 +190,16 @@ public class MinimapView: FlippedNSView {
180190
self?.updateContentViewHeight()
181191
self?.updateDocumentVisibleViewPosition()
182192
}
193+
194+
NotificationCenter.default.addObserver(
195+
forName: TextSelectionManager.selectionChangedNotification,
196+
object: textView?.selectionManager,
197+
queue: .main
198+
) { [weak self] _ in
199+
self?.selectionManager?.setSelectedRanges(
200+
self?.textView?.selectionManager.textSelections.map(\.range) ?? []
201+
)
202+
}
183203
}
184204

185205
required init?(coder: NSCoder) {

0 commit comments

Comments
 (0)