Skip to content

Commit b7f341f

Browse files
committed
Overscroll, Finalize Mouse Interaction, Document Everything
1 parent ef2f8f0 commit b7f341f

File tree

7 files changed

+114
-36
lines changed

7 files changed

+114
-36
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ struct ContentView: View {
4242
tabWidth: 4,
4343
lineHeight: 1.2,
4444
wrapLines: wrapLines,
45+
editorOverscroll: 0.3,
4546
cursorPositions: $cursorPositions,
4647
useThemeBackground: true,
4748
highlightProviders: [treeSitterClient],
4849
contentInsets: NSEdgeInsets(top: proxy.safeAreaInsets.top, left: 0, bottom: 28.0, right: 0),
49-
additionalTextInsets: NSEdgeInsets(top: 1, left: 0, bottom: proxy.size.height * 0.3, right: 0),
50+
additionalTextInsets: NSEdgeInsets(top: 1, left: 0, bottom: 1, right: 0),
5051
useSystemCursor: useSystemCursor
5152
)
5253
.overlay(alignment: .bottom) {

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public class TextViewController: NSViewController {
6868
highlighter?.invalidate()
6969
gutterView.textColor = theme.text.color.withAlphaComponent(0.35)
7070
gutterView.selectedLineTextColor = theme.text.color
71-
minimapView.theme = theme
71+
minimapView.setTheme(theme)
7272
}
7373
}
7474

Sources/CodeEditSourceEditor/Minimap/MinimapView+DocumentVisibleView.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@
88
import AppKit
99

1010
extension MinimapView {
11+
/// Updates the ``documentVisibleView`` and ``scrollView`` to match the editor's scroll offset.
12+
///
13+
/// - Note: In this context, the 'container' is the visible rect in the minimap.
14+
/// - Note: This is *tricky*, there's two cases for both views. If modifying, make sure to test both when the
15+
/// minimap is shorter than the container height and when the minimap should scroll.
16+
///
17+
/// The ``documentVisibleView`` uses a position that's entirely relative to the percent of the available scroll height scrolled.
18+
/// If the minimap is smaller than the container, it uses the same percent scrolled, but as a percent of the minimap height.
19+
///
20+
/// The height of the ``documentVisibleView`` is calculated using a ratio of the editor's height to the
21+
/// minimap's height, then applying that to the container's height.
22+
///
23+
/// The ``scrollView`` uses the scroll percentage calculated for the first case, and scrolls its content to that percentage.
24+
/// The ``scrollView`` is only modified if the minimap is longer than the container view.
1125
func updateDocumentVisibleViewPosition() {
1226
guard let textView = textView, let editorScrollView = textView.enclosingScrollView, let layoutManager else {
1327
return
@@ -17,7 +31,8 @@ extension MinimapView {
1731
let scrollPercentage = editorScrollView.percentScrolled
1832
guard scrollPercentage.isFinite else { return }
1933

20-
// Update Visible Pane, should scroll down slowly as the user scrolls the document, following the scroller.
34+
// Update Visible Pane, should scroll down slowly as the user scrolls the document, following a similar pace
35+
// as the vertical `NSScroller`.
2136
// Visible pane's height = scrollview visible height * (minimap line height / editor line height)
2237
// Visible pane's position = (container height - visible pane height) * scrollPercentage
2338
let visibleRectHeight = containerHeight * editorToMinimapHeightRatio

Sources/CodeEditSourceEditor/Minimap/MinimapView+TextLayoutManagerDelegate.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,25 @@ import AppKit
99
import CodeEditTextView
1010

1111
extension MinimapView: TextLayoutManagerDelegate {
12-
func layoutManagerHeightDidUpdate(newHeight: CGFloat) {
13-
contentView.frame.size.height = newHeight
12+
public func layoutManagerHeightDidUpdate(newHeight: CGFloat) {
13+
// contentView.frame.size.height = newHeight
14+
updateContentViewHeight()
1415
}
1516

16-
func layoutManagerMaxWidthDidChange(newWidth: CGFloat) { }
17+
public func layoutManagerMaxWidthDidChange(newWidth: CGFloat) { }
1718

18-
func layoutManagerTypingAttributes() -> [NSAttributedString.Key: Any] {
19+
public func layoutManagerTypingAttributes() -> [NSAttributedString.Key: Any] {
1920
textView?.layoutManagerTypingAttributes() ?? [:]
2021
}
2122

22-
func textViewportSize() -> CGSize {
23+
public func textViewportSize() -> CGSize {
2324
var size = scrollView.contentSize
2425
size.height -= scrollView.contentInsets.top + scrollView.contentInsets.bottom
2526
size.width = textView?.layoutManager.maxLineLayoutWidth ?? size.width
2627
return size
2728
}
2829

29-
func layoutManagerYAdjustment(_ yAdjustment: CGFloat) {
30+
public func layoutManagerYAdjustment(_ yAdjustment: CGFloat) {
3031
var point = scrollView.documentVisibleRect.origin
3132
point.y += yAdjustment
3233
scrollView.documentView?.scroll(point)

Sources/CodeEditSourceEditor/Minimap/MinimapView.swift

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,35 @@
88
import AppKit
99
import CodeEditTextView
1010

11-
class MinimapView: FlippedNSView {
11+
/// The minimap view displays a copy of editor contents as a series of small bubbles in place of text.
12+
///
13+
/// This view consists of the following subviews in order
14+
/// ```
15+
/// MinimapView
16+
/// |-> separatorView: A small, grey, leading, separator that distinguishes the minimap from other content.
17+
/// |-> documentVisibleView: Displays a rectangle that represents the portion of the minimap visible in the editor's
18+
/// | visible rect. This is draggable and responds to the editor's height.
19+
/// |-> scrollView: Container for the summary bubbles
20+
/// | |-> contentView: Target view for the summary bubble content
21+
/// ```
22+
///
23+
/// To keep contents in sync with the text view, this view requires that its ``scrollView`` have the same vertical
24+
/// content insets as the editor's content insets.
25+
///
26+
/// The minimap can be styled using an ``EditorTheme``. See ``setTheme(_:)`` for use and colors used by this view.
27+
public class MinimapView: FlippedNSView {
1228
weak var textView: TextView?
1329

1430
/// The container scrollview for the minimap contents.
15-
let scrollView: ForwardingScrollView
31+
public let scrollView: ForwardingScrollView
1632
/// The view text lines are rendered into.
17-
let contentView: FlippedNSView
33+
public let contentView: FlippedNSView
1834
/// The box displaying the visible region on the minimap.
19-
let documentVisibleView: NSView
35+
public let documentVisibleView: NSView
2036
/// A small gray line on the left of the minimap distinguishing it from the editor.
21-
let separatorView: NSView
37+
public let separatorView: NSView
2238

39+
/// Responder for a drag gesture on the ``documentVisibleView``.
2340
var documentVisibleViewDragGesture: NSPanGestureRecognizer?
2441

2542
/// The layout manager that uses the ``lineRenderer`` to render and layout lines.
@@ -28,35 +45,33 @@ class MinimapView: FlippedNSView {
2845
/// using ``MinimapLineFragmentView``
2946
let lineRenderer: MinimapLineRenderer
3047

31-
var theme: EditorTheme {
32-
didSet {
33-
documentVisibleView.layer?.backgroundColor = theme.text.color.withAlphaComponent(0.05).cgColor
34-
layer?.backgroundColor = theme.background.cgColor
35-
}
36-
}
48+
// MARK: - Calculated Variables
3749

3850
var minimapHeight: CGFloat {
3951
contentView.frame.height
4052
}
4153

4254
var editorHeight: CGFloat {
43-
textView?.frame.height ?? 0.0
55+
textView?.layoutManager.estimatedHeight() ?? 1.0
4456
}
4557

4658
var editorToMinimapHeightRatio: CGFloat {
4759
minimapHeight / editorHeight
4860
}
4961

62+
/// The height of the available container, less the scroll insets to reflect the visible height.
5063
var containerHeight: CGFloat {
51-
(textView?.enclosingScrollView?.visibleRect.height ?? 0.0)
52-
- (textView?.enclosingScrollView?.contentInsets.vertical ?? 0.0)
64+
scrollView.visibleRect.height - scrollView.contentInsets.vertical
5365
}
5466

5567
// MARK: - Init
5668

57-
init(textView: TextView, theme: EditorTheme) {
69+
/// Creates a minimap view with the text view to track, and an initial theme.
70+
/// - Parameters:
71+
/// - textView: The text view to match contents with.
72+
/// - theme: The theme for the minimap to use.
73+
public init(textView: TextView, theme: EditorTheme) {
5874
self.textView = textView
59-
self.theme = theme
6075
self.lineRenderer = MinimapLineRenderer(textView: textView)
6176

6277
self.scrollView = ForwardingScrollView()
@@ -67,7 +82,7 @@ class MinimapView: FlippedNSView {
6782
scrollView.verticalScrollElasticity = .none
6883
scrollView.receiver = textView.enclosingScrollView
6984

70-
self.contentView = FlippedNSView(frame: .zero)
85+
self.contentView = FlippedNSView()
7186
contentView.translatesAutoresizingMaskIntoConstraints = false
7287

7388
self.documentVisibleView = NSView()
@@ -162,6 +177,7 @@ class MinimapView: FlippedNSView {
162177
queue: .main
163178
) { [weak self] _ in
164179
// Frame changed
180+
self?.updateContentViewHeight()
165181
self?.updateDocumentVisibleViewPosition()
166182
}
167183
}
@@ -176,18 +192,59 @@ class MinimapView: FlippedNSView {
176192
return rect.pixelAligned
177193
}
178194

179-
override func layout() {
195+
override public func resetCursorRects() {
196+
// Don't use an iBeam
197+
addCursorRect(bounds, cursor: .arrow)
198+
}
199+
200+
override public func layout() {
180201
layoutManager?.layoutLines()
181202
super.layout()
182203
}
183204

184-
override func hitTest(_ point: NSPoint) -> NSView? {
205+
override public func hitTest(_ point: NSPoint) -> NSView? {
206+
guard let point = superview?.convert(point, to: self) else { return nil }
207+
// For performance, don't hitTest the layout fragment views, but make sure the `documentVisibleView` is
208+
// hittable.
185209
if documentVisibleView.frame.contains(point) {
186210
return documentVisibleView
187211
} else if visibleRect.contains(point) {
188-
return textView
212+
return self
189213
} else {
190214
return super.hitTest(point)
191215
}
192216
}
217+
218+
// Eat mouse events so we don't pass them on to the text view. Leads to some odd behavior.
219+
220+
override public func mouseDown(with event: NSEvent) { }
221+
override public func mouseDragged(with event: NSEvent) { }
222+
223+
/// Sets the content view height, matching the text view's overscroll setting as well as the layout manager's
224+
/// cached height.
225+
func updateContentViewHeight() {
226+
guard let estimatedContentHeight = layoutManager?.estimatedHeight(),
227+
let overscrollAmount = textView?.overscrollAmount else {
228+
return
229+
}
230+
let overscroll = containerHeight * overscrollAmount * editorToMinimapHeightRatio
231+
let height = estimatedContentHeight + overscroll
232+
233+
// Only update a frame if needed
234+
if contentView.frame.height != height {
235+
contentView.frame.size.height = height
236+
}
237+
}
238+
239+
/// Updates the minimap to reflect a new theme.
240+
///
241+
/// Colors used:
242+
/// - ``documentVisibleView``'s background color = `theme.text` with `0.05` alpha.
243+
/// - The minimap's background color = `theme.background`.
244+
///
245+
/// - Parameter theme: The selected theme.
246+
public func setTheme(_ theme: EditorTheme) {
247+
documentVisibleView.layer?.backgroundColor = theme.text.color.withAlphaComponent(0.05).cgColor
248+
layer?.backgroundColor = theme.background.cgColor
249+
}
193250
}

Sources/CodeEditSourceEditor/SupportingViews/FlippedNSView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77

88
import AppKit
99

10-
class FlippedNSView: NSView {
11-
override var isFlipped: Bool { true }
10+
open class FlippedNSView: NSView {
11+
open override var isFlipped: Bool { true }
1212
}

Sources/CodeEditSourceEditor/SupportingViews/ForwardingScrollView.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77

88
import Cocoa
99

10-
class ForwardingScrollView: NSScrollView {
10+
/// A custom ``NSScrollView`` subclass that forwards scroll wheel events to another scroll view.
11+
/// This class does not process any other scrolling events. However, it still lays out it's contents like a
12+
/// regular scroll view.
13+
///
14+
/// Set ``receiver`` to target events.
15+
open class ForwardingScrollView: NSScrollView {
16+
/// The target scroll view to send scroll events to.
17+
open weak var receiver: NSScrollView?
1118

12-
weak var receiver: NSScrollView?
13-
14-
override func scrollWheel(with event: NSEvent) {
19+
open override func scrollWheel(with event: NSEvent) {
1520
receiver?.scrollWheel(with: event)
1621
}
17-
1822
}

0 commit comments

Comments
 (0)