From 2473ed9eb8cb039de22b552b18f145db5d497e68 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:18:46 -0600 Subject: [PATCH 1/4] Adjust Gutter Background, Add Trailing Padding --- .../Controller/TextViewController.swift | 11 ++++- .../Gutter/GutterView.swift | 49 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift index 4dbf282a2..5bcf7e376 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift @@ -93,6 +93,10 @@ public class TextViewController: NSViewController { didSet { textView.layoutManager.wrapLines = wrapLines scrollView.hasHorizontalScroller = !wrapLines + textView.edgeInsets = HorizontalEdgeInsets( + left: textView.edgeInsets.left, + right: textViewTrailingInset // Refresh this value, see docs + ) } } @@ -193,6 +197,11 @@ public class TextViewController: NSViewController { return max(inset, .zero) } + + /// The trailing inset for the editor. Grows when line wrapping is disabled. + package var textViewTrailingInset: CGFloat { + wrapLines ? 1 : 48 + } // MARK: Init @@ -315,6 +324,6 @@ public class TextViewController: NSViewController { extension TextViewController: GutterViewDelegate { public func gutterViewWidthDidUpdate(newWidth: CGFloat) { gutterView?.frame.size.width = newWidth - textView?.edgeInsets = HorizontalEdgeInsets(left: newWidth, right: 0) + textView?.edgeInsets = HorizontalEdgeInsets(left: newWidth, right: textViewTrailingInset) } } diff --git a/Sources/CodeEditSourceEditor/Gutter/GutterView.swift b/Sources/CodeEditSourceEditor/Gutter/GutterView.swift index 1cf44ea7e..c6be68a4e 100644 --- a/Sources/CodeEditSourceEditor/Gutter/GutterView.swift +++ b/Sources/CodeEditSourceEditor/Gutter/GutterView.swift @@ -13,6 +13,24 @@ public protocol GutterViewDelegate: AnyObject { func gutterViewWidthDidUpdate(newWidth: CGFloat) } +/// The gutter view displays line numbers that match the text view's line indexes. +/// This view is used as a scroll view's ruler view. It sits on top of the text view so text scrolls underneath the +/// gutter if line wrapping is disabled. +/// +/// If the gutter needs more space (when the number of digits in the numbers increases eg. adding a line after line 99), +/// it will notify it's delegate via the ``GutterViewDelegate/gutterViewWidthDidUpdate(newWidth:)`` method. In +/// `CodeEditSourceEditor`, this notifies the ``TextViewController``, which in turn updates the textview's edge insets +/// to adjust for the new leading inset. +/// +/// This view also listens for selection updates, and draws a selected background on selected lines to keep the illusion +/// that the gutter's line numbers are inline with the line itself. +/// +/// The gutter view has insets of it's own that are relative to the widest line index. By default, these insets are 20px +/// leading, and 12px trailing. However, this view also has a ``GutterView/backgroundEdgeInsets`` property, that pads +/// the rect that has a background drawn. This allows the text to be scrolled under the gutter view for 8px before being +/// overlapped by the gutter. It should help the textview keep the cursor visible if the user types while the cursor is +/// off the leading edge of the editor. +/// public class GutterView: NSView { struct EdgeInsets: Equatable, Hashable { let leading: CGFloat @@ -32,6 +50,9 @@ public class GutterView: NSView { @Invalidating(.display) var edgeInsets: EdgeInsets = EdgeInsets(leading: 20, trailing: 12) + @Invalidating(.display) + var backgroundEdgeInsets: EdgeInsets = EdgeInsets(leading: 0, trailing: 8) + @Invalidating(.display) var backgroundColor: NSColor? = NSColor.controlBackgroundColor @@ -44,6 +65,7 @@ public class GutterView: NSView { @Invalidating(.display) var selectedLineColor: NSColor = NSColor.selectedTextBackgroundColor.withSystemEffect(.disabled) + /// The required width of the entire gutter, including padding. private(set) public var gutterWidth: CGFloat = 0 private weak var textView: TextView? @@ -118,6 +140,17 @@ public class GutterView: NSView { } } + private func drawBackground(_ context: CGContext) { + guard let backgroundColor else { return } + let xPos = backgroundEdgeInsets.leading + let width = gutterWidth - backgroundEdgeInsets.trailing + + context.saveGState() + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(x: xPos, y: 0, width: width, height: frame.height)) + context.restoreGState() + } + private func drawSelectedLines(_ context: CGContext) { guard let textView = textView, let selectionManager = textView.selectionManager, @@ -126,10 +159,14 @@ public class GutterView: NSView { return } context.saveGState() + var highlightedLines: Set = [] context.setFillColor(selectedLineColor.cgColor) - for selection in selectionManager.textSelections - where selection.range.isEmpty { + + let xPos = backgroundEdgeInsets.leading + let width = gutterWidth - backgroundEdgeInsets.trailing + + for selection in selectionManager.textSelections where selection.range.isEmpty { guard let line = textView.layoutManager.textLineForOffset(selection.range.location), visibleRange.intersection(line.range) != nil || selection.range.location == textView.length, !highlightedLines.contains(line.data.id) else { @@ -138,13 +175,14 @@ public class GutterView: NSView { highlightedLines.insert(line.data.id) context.fill( CGRect( - x: 0.0, + x: xPos, y: line.yPos, - width: maxWidth + edgeInsets.horizontal, + width: width, height: line.height ) ) } + context.restoreGState() } @@ -204,8 +242,9 @@ public class GutterView: NSView { CATransaction.begin() superview?.clipsToBounds = false superview?.layer?.masksToBounds = false - layer?.backgroundColor = backgroundColor?.cgColor +// layer?.backgroundColor = backgroundColor?.cgColor updateWidthIfNeeded() + drawBackground(context) drawSelectedLines(context) drawLineNumbers(context) CATransaction.commit() From 7b480f54b654ea58155f092d70366703ff0cee7b Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:32:12 -0600 Subject: [PATCH 2/4] Lint --- .../Documents/CodeEditSourceEditorExampleDocument.swift | 4 ++-- .../CodeEditSourceEditor/Controller/TextViewController.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Documents/CodeEditSourceEditorExampleDocument.swift b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Documents/CodeEditSourceEditorExampleDocument.swift index ab52c5698..7ca5bca15 100644 --- a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Documents/CodeEditSourceEditorExampleDocument.swift +++ b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Documents/CodeEditSourceEditorExampleDocument.swift @@ -25,11 +25,11 @@ struct CodeEditSourceEditorExampleDocument: FileDocument { guard let data = configuration.file.regularFileContents else { throw CocoaError(.fileReadCorruptFile) } - text = String(decoding: data, as: UTF8.self) + text = String(data: data, encoding: .utf8) } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { - let data = text.data(using: .utf8)! + let data = Data(text.utf8) return .init(regularFileWithContents: data) } } diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift index 5bcf7e376..57baea8b9 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift @@ -197,7 +197,7 @@ public class TextViewController: NSViewController { return max(inset, .zero) } - + /// The trailing inset for the editor. Grows when line wrapping is disabled. package var textViewTrailingInset: CGFloat { wrapLines ? 1 : 48 From d8f93512c87a43bea352a8201e02997e8d1ebf1b Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:06:32 -0600 Subject: [PATCH 3/4] Lint (again) --- .swiftlint.yml | 1 + .../Documents/CodeEditSourceEditorExampleDocument.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index ea0c8485d..9a33d09a9 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -5,6 +5,7 @@ disabled_rules: - todo - trailing_comma - nesting + - optional_data_string_conversion type_name: excluded: diff --git a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Documents/CodeEditSourceEditorExampleDocument.swift b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Documents/CodeEditSourceEditorExampleDocument.swift index 7ca5bca15..9e573685a 100644 --- a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Documents/CodeEditSourceEditorExampleDocument.swift +++ b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Documents/CodeEditSourceEditorExampleDocument.swift @@ -25,7 +25,7 @@ struct CodeEditSourceEditorExampleDocument: FileDocument { guard let data = configuration.file.regularFileContents else { throw CocoaError(.fileReadCorruptFile) } - text = String(data: data, encoding: .utf8) + text = String(decoding: data, as: UTF8.self) } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { From 4f169e510ba479ce412a4e8c7ccad45efff819c5 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:06:47 -0600 Subject: [PATCH 4/4] Remove Comment --- Sources/CodeEditSourceEditor/Gutter/GutterView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/CodeEditSourceEditor/Gutter/GutterView.swift b/Sources/CodeEditSourceEditor/Gutter/GutterView.swift index c6be68a4e..31568d4a1 100644 --- a/Sources/CodeEditSourceEditor/Gutter/GutterView.swift +++ b/Sources/CodeEditSourceEditor/Gutter/GutterView.swift @@ -242,7 +242,6 @@ public class GutterView: NSView { CATransaction.begin() superview?.clipsToBounds = false superview?.layer?.masksToBounds = false -// layer?.backgroundColor = backgroundColor?.cgColor updateWidthIfNeeded() drawBackground(context) drawSelectedLines(context)