From f1888f0bfd8d4121050f8910f6e3aea7b43ca666 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 12 Jan 2025 14:05:57 -0600 Subject: [PATCH 1/8] Add Text Insets --- .../project.pbxproj | 4 +- .../Views/ContentView.swift | 11 ++- .../Views/SwiftUITextView.swift | 7 +- .../Views/TextViewController.swift | 24 +++++ .../TextSelectionManager+Draw.swift | 97 +++++++++++++++++++ .../TextSelectionManager+FillRects.swift | 4 +- .../TextSelectionManager.swift | 90 ++--------------- .../CodeEditTextView/TextView/TextView.swift | 21 +++- .../Utils/HorizontalEdgeInsets.swift | 10 +- 9 files changed, 175 insertions(+), 93 deletions(-) create mode 100644 Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Draw.swift diff --git a/Example/CodeEditTextViewExample/CodeEditTextViewExample.xcodeproj/project.pbxproj b/Example/CodeEditTextViewExample/CodeEditTextViewExample.xcodeproj/project.pbxproj index 97a0dffc1..4a583aa28 100644 --- a/Example/CodeEditTextViewExample/CodeEditTextViewExample.xcodeproj/project.pbxproj +++ b/Example/CodeEditTextViewExample/CodeEditTextViewExample.xcodeproj/project.pbxproj @@ -189,7 +189,7 @@ CODE_SIGN_ENTITLEMENTS = CodeEditTextViewExample/CodeEditTextViewExample.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"CodeEditTextViewExample/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -226,7 +226,7 @@ CODE_SIGN_ENTITLEMENTS = CodeEditTextViewExample/CodeEditTextViewExample.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"CodeEditTextViewExample/Preview Content\""; + DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; diff --git a/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/ContentView.swift b/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/ContentView.swift index f914a802c..c6b0f4f0f 100644 --- a/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/ContentView.swift +++ b/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/ContentView.swift @@ -9,9 +9,18 @@ import SwiftUI struct ContentView: View { @Binding var document: CodeEditTextViewExampleDocument + @AppStorage("wraplines") private var wrapLines: Bool = true + @AppStorage("edgeinsets") private var enableEdgeInsets: Bool = false var body: some View { - SwiftUITextView(text: $document.text) + VStack(spacing: 0) { + HStack { + Toggle("Wrap Lines", isOn: $wrapLines) + Toggle("Inset Edges", isOn: $enableEdgeInsets) + } + Divider() + SwiftUITextView(text: $document.text, wrapLines: $wrapLines, enableEdgeInsets: $enableEdgeInsets) + } } } diff --git a/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/SwiftUITextView.swift b/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/SwiftUITextView.swift index 8dd341c23..96d5d732b 100644 --- a/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/SwiftUITextView.swift +++ b/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/SwiftUITextView.swift @@ -11,15 +11,20 @@ import CodeEditTextView struct SwiftUITextView: NSViewControllerRepresentable { @Binding var text: String + @Binding var wrapLines: Bool + @Binding var enableEdgeInsets: Bool func makeNSViewController(context: Context) -> TextViewController { let controller = TextViewController(string: text) context.coordinator.controller = controller + controller.wrapLines = wrapLines + controller.enableEdgeInsets = enableEdgeInsets return controller } func updateNSViewController(_ nsViewController: TextViewController, context: Context) { - // Do nothing, our binding has to be a one-way binding + nsViewController.wrapLines = wrapLines + nsViewController.enableEdgeInsets = enableEdgeInsets } func makeCoordinator() -> Coordinator { diff --git a/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/TextViewController.swift b/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/TextViewController.swift index 0e568b55f..a880d9731 100644 --- a/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/TextViewController.swift +++ b/Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/TextViewController.swift @@ -11,6 +11,22 @@ import CodeEditTextView class TextViewController: NSViewController { var scrollView: NSScrollView! var textView: TextView! + var enableEdgeInsets: Bool = false { + didSet { + if enableEdgeInsets { + textView.edgeInsets = .init(left: 20, right: 30) + textView.textInsets = .init(left: 10, right: 30) + } else { + textView.edgeInsets = .zero + textView.textInsets = .zero + } + } + } + var wrapLines: Bool = true { + didSet { + textView.wrapLines = wrapLines + } + } init(string: String) { textView = TextView(string: string) @@ -24,6 +40,14 @@ class TextViewController: NSViewController { override func loadView() { scrollView = NSScrollView() textView.translatesAutoresizingMaskIntoConstraints = false + textView.wrapLines = wrapLines + if enableEdgeInsets { + textView.edgeInsets = .init(left: 30, right: 30) + textView.textInsets = .init(left: 0, right: 30) + } else { + textView.edgeInsets = .zero + textView.textInsets = .zero + } scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.documentView = textView diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Draw.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Draw.swift new file mode 100644 index 000000000..b52fdc781 --- /dev/null +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Draw.swift @@ -0,0 +1,97 @@ +// +// File.swift +// CodeEditTextView +// +// Created by Khan Winter on 1/12/25. +// + +import AppKit + +extension TextSelectionManager { + /// Draws line backgrounds and selection rects for each selection in the given rect. + /// - Parameter rect: The rect to draw in. + func drawSelections(in rect: NSRect) { + guard let context = NSGraphicsContext.current?.cgContext else { return } + context.saveGState() + var highlightedLines: Set = [] + // For each selection in the rect + for textSelection in textSelections { + if textSelection.range.isEmpty { + drawHighlightedLine( + in: rect, + for: textSelection, + context: context, + highlightedLines: &highlightedLines + ) + } else { + drawSelectedRange(in: rect, for: textSelection, context: context) + } + } + context.restoreGState() + } + + /// Draws a highlighted line in the given rect. + /// - Parameters: + /// - rect: The rect to draw in. + /// - textSelection: The selection to draw. + /// - context: The context to draw in. + /// - highlightedLines: The set of all lines that have already been highlighted, used to avoid highlighting lines + /// twice and updated if this function comes across a new line id. + private func drawHighlightedLine( + in rect: NSRect, + for textSelection: TextSelection, + context: CGContext, + highlightedLines: inout Set + ) { + guard let linePosition = layoutManager?.textLineForOffset(textSelection.range.location), + !highlightedLines.contains(linePosition.data.id) else { + return + } + highlightedLines.insert(linePosition.data.id) + context.saveGState() + + let insetXPos = max(rect.minX, edgeInsets.left) + let maxWidth = (textView?.frame.width ?? 0) - insetXPos - edgeInsets.right + + let selectionRect = CGRect( + x: insetXPos, + y: linePosition.yPos, + width: min(rect.width, maxWidth), + height: linePosition.height + ).pixelAligned + + if selectionRect.intersects(rect) { + context.setFillColor(selectedLineBackgroundColor.cgColor) + context.fill(selectionRect) + } + context.restoreGState() + } + + /// Draws a selected range in the given context. + /// - Parameters: + /// - rect: The rect to draw in. + /// - range: The range to highlight. + /// - context: The context to draw in. + private func drawSelectedRange(in rect: NSRect, for textSelection: TextSelection, context: CGContext) { + context.saveGState() + + let fillColor = (textView?.isFirstResponder ?? false) + ? selectionBackgroundColor.cgColor + : selectionBackgroundColor.grayscale.cgColor + + context.setFillColor(fillColor) + + let fillRects = getFillRects(in: rect, for: textSelection) + + let minX = fillRects.min(by: { $0.origin.x < $1.origin.x })?.origin.x ?? 0 + let minY = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin.y ?? 0 + let max = fillRects.max(by: { $0.maxY < $1.maxY }) ?? .zero + let origin = CGPoint(x: minX, y: minY) + let size = CGSize(width: max.maxX - minX, height: max.maxY - minY) + textSelection.boundingRect = CGRect(origin: origin, size: size) + + context.fill(fillRects) + context.restoreGState() + } + +} diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift index 46f56758e..640cc4091 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift @@ -30,8 +30,8 @@ extension TextSelectionManager { return [] } - let insetXPos = max(layoutManager.edgeInsets.left, rect.minX) - let insetWidth = max(0, rect.maxX - insetXPos - layoutManager.edgeInsets.right) + let insetXPos = max(edgeInsets.left, rect.minX) + let insetWidth = max(0, rect.maxX - insetXPos - edgeInsets.right) let insetRect = NSRect(x: insetXPos, y: rect.origin.y, width: insetWidth, height: rect.height) // Calculate the first line and any rects selected diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift index 546e856ff..b6311a9ab 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift @@ -38,6 +38,13 @@ public class TextSelectionManager: NSObject { } } + /// Determines how far inset to draw selection content. + public var edgeInsets: HorizontalEdgeInsets = .zero { + didSet { + delegate?.setNeedsDisplay() + } + } + internal(set) public var textSelections: [TextSelection] = [] weak var layoutManager: TextLayoutManager? weak var textStorage: NSTextStorage? @@ -224,87 +231,4 @@ public class TextSelectionManager: NSObject { textSelection.view?.removeFromSuperview() } } - - // MARK: - Draw - - /// Draws line backgrounds and selection rects for each selection in the given rect. - /// - Parameter rect: The rect to draw in. - func drawSelections(in rect: NSRect) { - guard let context = NSGraphicsContext.current?.cgContext else { return } - context.saveGState() - var highlightedLines: Set = [] - // For each selection in the rect - for textSelection in textSelections { - if textSelection.range.isEmpty { - drawHighlightedLine( - in: rect, - for: textSelection, - context: context, - highlightedLines: &highlightedLines - ) - } else { - drawSelectedRange(in: rect, for: textSelection, context: context) - } - } - context.restoreGState() - } - - /// Draws a highlighted line in the given rect. - /// - Parameters: - /// - rect: The rect to draw in. - /// - textSelection: The selection to draw. - /// - context: The context to draw in. - /// - highlightedLines: The set of all lines that have already been highlighted, used to avoid highlighting lines - /// twice and updated if this function comes across a new line id. - private func drawHighlightedLine( - in rect: NSRect, - for textSelection: TextSelection, - context: CGContext, - highlightedLines: inout Set - ) { - guard let linePosition = layoutManager?.textLineForOffset(textSelection.range.location), - !highlightedLines.contains(linePosition.data.id) else { - return - } - highlightedLines.insert(linePosition.data.id) - context.saveGState() - let selectionRect = CGRect( - x: rect.minX, - y: linePosition.yPos, - width: rect.width, - height: linePosition.height - ) - if selectionRect.intersects(rect) { - context.setFillColor(selectedLineBackgroundColor.cgColor) - context.fill(selectionRect) - } - context.restoreGState() - } - - /// Draws a selected range in the given context. - /// - Parameters: - /// - rect: The rect to draw in. - /// - range: The range to highlight. - /// - context: The context to draw in. - private func drawSelectedRange(in rect: NSRect, for textSelection: TextSelection, context: CGContext) { - context.saveGState() - - let fillColor = (textView?.isFirstResponder ?? false) - ? selectionBackgroundColor.cgColor - : selectionBackgroundColor.grayscale.cgColor - - context.setFillColor(fillColor) - - let fillRects = getFillRects(in: rect, for: textSelection) - - let minX = fillRects.min(by: { $0.origin.x < $1.origin.x })?.origin.x ?? 0 - let minY = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin.y ?? 0 - let max = fillRects.max(by: { $0.maxY < $1.maxY }) ?? .zero - let origin = CGPoint(x: minX, y: minY) - let size = CGSize(width: max.maxX - minX, height: max.maxY - minY) - textSelection.boundingRect = CGRect(origin: origin, size: size) - - context.fill(fillRects) - context.restoreGState() - } } diff --git a/Sources/CodeEditTextView/TextView/TextView.swift b/Sources/CodeEditTextView/TextView/TextView.swift index 48fc75c5e..df8961175 100644 --- a/Sources/CodeEditTextView/TextView/TextView.swift +++ b/Sources/CodeEditTextView/TextView/TextView.swift @@ -150,13 +150,28 @@ public class TextView: NSView, NSTextContent { } } - /// The edge insets for the text view. + /// The edge insets for the text view. This value insets every piece of drawable content in the view, including + /// selection rects. + /// + /// To further inset the text from the edge, without modifying how selections are inset, use ``textInsets`` public var edgeInsets: HorizontalEdgeInsets { get { - layoutManager?.edgeInsets ?? .zero + selectionManager.edgeInsets + } + set { + layoutManager.edgeInsets = newValue + textInsets + selectionManager.edgeInsets = newValue + } + } + + /// Insets just drawn text from the horizontal edges. This is in addition to the insets in ``edgeInsets``, but does + /// not apply to other drawn content. + public var textInsets: HorizontalEdgeInsets { + get { + layoutManager.edgeInsets - selectionManager.edgeInsets } set { - layoutManager?.edgeInsets = newValue + layoutManager.edgeInsets = edgeInsets + newValue } } diff --git a/Sources/CodeEditTextView/Utils/HorizontalEdgeInsets.swift b/Sources/CodeEditTextView/Utils/HorizontalEdgeInsets.swift index 47427938c..652010085 100644 --- a/Sources/CodeEditTextView/Utils/HorizontalEdgeInsets.swift +++ b/Sources/CodeEditTextView/Utils/HorizontalEdgeInsets.swift @@ -7,7 +7,7 @@ import Foundation -public struct HorizontalEdgeInsets: Codable, Sendable, Equatable { +public struct HorizontalEdgeInsets: Codable, Sendable, Equatable, AdditiveArithmetic { public var left: CGFloat public var right: CGFloat @@ -29,4 +29,12 @@ public struct HorizontalEdgeInsets: Codable, Sendable, Equatable { public static let zero: HorizontalEdgeInsets = { HorizontalEdgeInsets(left: 0, right: 0) }() + + public static func + (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> HorizontalEdgeInsets { + HorizontalEdgeInsets(left: lhs.left + rhs.left, right: lhs.right + rhs.right) + } + + public static func - (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> HorizontalEdgeInsets { + HorizontalEdgeInsets(left: lhs.left - rhs.left, right: lhs.right - rhs.right) + } } From f77ae4afc10165d9343edb66bc98cc25da8cb183 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 12 Jan 2025 14:15:41 -0600 Subject: [PATCH 2/8] Move some file for linter --- .../TextView/TextView+SetText.swift | 40 +++++++++++++++++++ .../CodeEditTextView/TextView/TextView.swift | 36 ++--------------- 2 files changed, 43 insertions(+), 33 deletions(-) create mode 100644 Sources/CodeEditTextView/TextView/TextView+SetText.swift diff --git a/Sources/CodeEditTextView/TextView/TextView+SetText.swift b/Sources/CodeEditTextView/TextView/TextView+SetText.swift new file mode 100644 index 000000000..7581dbcd7 --- /dev/null +++ b/Sources/CodeEditTextView/TextView/TextView+SetText.swift @@ -0,0 +1,40 @@ +// +// TextView+SetText.swift +// CodeEditTextView +// +// Created by Khan Winter on 1/12/25. +// + +import AppKit + +extension TextView { + /// Sets the text view's text to a new value. + /// - Parameter text: The new contents of the text view. + public func setText(_ text: String) { + let newStorage = NSTextStorage(string: text) + self.setTextStorage(newStorage) + } + + /// Set a new text storage object for the view. + /// - Parameter textStorage: The new text storage to use. + public func setTextStorage(_ textStorage: NSTextStorage) { + self.textStorage = textStorage + + subviews.forEach { view in + view.removeFromSuperview() + } + + textStorage.addAttributes(typingAttributes, range: documentRange) + layoutManager.textStorage = textStorage + layoutManager.reset() + + selectionManager.textStorage = textStorage + selectionManager.setSelectedRanges(selectionManager.textSelections.map { $0.range }) + + _undoManager?.clearStack() + + textStorage.delegate = storageDelegate + needsDisplay = true + needsLayout = true + } +} diff --git a/Sources/CodeEditTextView/TextView/TextView.swift b/Sources/CodeEditTextView/TextView/TextView.swift index df8961175..84a0a46d6 100644 --- a/Sources/CodeEditTextView/TextView/TextView.swift +++ b/Sources/CodeEditTextView/TextView/TextView.swift @@ -234,11 +234,11 @@ public class TextView: NSView, NSTextContent { /// - Warning: Do not update the text storage object directly. Doing so will very likely break the text view's /// layout system. Use methods like ``TextView/replaceCharacters(in:with:)-58mt7`` or /// ``TextView/insertText(_:)`` to modify content. - private(set) public var textStorage: NSTextStorage! + package(set) public var textStorage: NSTextStorage! /// The layout manager for the text view. - private(set) public var layoutManager: TextLayoutManager! + package(set) public var layoutManager: TextLayoutManager! /// The selection manager for the text view. - private(set) public var selectionManager: TextSelectionManager! + package(set) public var selectionManager: TextSelectionManager! /// Empasizse text ranges in the text view public var emphasizeAPI: EmphasizeAPI? @@ -325,36 +325,6 @@ public class TextView: NSView, NSTextContent { setUpDragGesture() } - /// Sets the text view's text to a new value. - /// - Parameter text: The new contents of the text view. - public func setText(_ text: String) { - let newStorage = NSTextStorage(string: text) - self.setTextStorage(newStorage) - } - - /// Set a new text storage object for the view. - /// - Parameter textStorage: The new text storage to use. - public func setTextStorage(_ textStorage: NSTextStorage) { - self.textStorage = textStorage - - subviews.forEach { view in - view.removeFromSuperview() - } - - textStorage.addAttributes(typingAttributes, range: documentRange) - layoutManager.textStorage = textStorage - layoutManager.reset() - - selectionManager.textStorage = textStorage - selectionManager.setSelectedRanges(selectionManager.textSelections.map { $0.range }) - - _undoManager?.clearStack() - - textStorage.delegate = storageDelegate - needsDisplay = true - needsLayout = true - } - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } From 771f6a8329874bc39a0df123a7f1dcd14cd3dcb4 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:08:34 -0600 Subject: [PATCH 3/8] File Header Fix --- .../TextSelectionManager/TextSelectionManager+Draw.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Draw.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Draw.swift index b52fdc781..e79832fdc 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Draw.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+Draw.swift @@ -1,5 +1,5 @@ // -// File.swift +// TextSelectionManager+Draw.swift // CodeEditTextView // // Created by Khan Winter on 1/12/25. From 8834ce4a1832497e7e0fa96a0a384f9dc5555586 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:50:27 -0600 Subject: [PATCH 4/8] Disambiguate Selection Rect Drawing --- .../TextSelectionManager+FillRects.swift | 29 ++----------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift index 640cc4091..dc073d1b9 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift @@ -23,38 +23,13 @@ extension TextSelectionManager { let range = textSelection.range var fillRects: [CGRect] = [] - guard let firstLinePosition = layoutManager.lineStorage.getLine(atOffset: range.location), - let lastLinePosition = range.max == layoutManager.lineStorage.length - ? layoutManager.lineStorage.last - : layoutManager.lineStorage.getLine(atOffset: range.max) else { - return [] - } let insetXPos = max(edgeInsets.left, rect.minX) let insetWidth = max(0, rect.maxX - insetXPos - edgeInsets.right) let insetRect = NSRect(x: insetXPos, y: rect.origin.y, width: insetWidth, height: rect.height) - // Calculate the first line and any rects selected - // If the last line position is not the same as the first, calculate any rects from that line. - // If there's > 0 space between the first and last positions, add a rect between them to cover any - // intermediate lines. - - let firstLineRects = getFillRects(in: rect, selectionRange: range, forPosition: firstLinePosition) - let lastLineRects: [CGRect] = if lastLinePosition.range != firstLinePosition.range { - getFillRects(in: rect, selectionRange: range, forPosition: lastLinePosition) - } else { - [] - } - - fillRects.append(contentsOf: firstLineRects + lastLineRects) - - if firstLinePosition.yPos + firstLinePosition.height < lastLinePosition.yPos { - fillRects.append(CGRect( - x: insetXPos, - y: firstLinePosition.yPos + firstLinePosition.height, - width: insetWidth, - height: lastLinePosition.yPos - (firstLinePosition.yPos + firstLinePosition.height) - )) + for linePosition in layoutManager.lineStorage.linesInRange(range) { + fillRects.append(contentsOf: getFillRects(in: insetRect, selectionRange: range, forPosition: linePosition)) } // Pixel align these to avoid aliasing on the edges of each rect that should be a solid box. From e56b346e127691dde4f67fb0a97c25991baf3a63 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:57:18 -0600 Subject: [PATCH 5/8] Selection Boxes Use Text Drawing Space --- .../TextSelectionManager/TextSelectionManager+FillRects.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift index dc073d1b9..f45fff7bf 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift @@ -25,7 +25,7 @@ extension TextSelectionManager { var fillRects: [CGRect] = [] let insetXPos = max(edgeInsets.left, rect.minX) - let insetWidth = max(0, rect.maxX - insetXPos - edgeInsets.right) + let insetWidth = max(0, rect.maxX - insetXPos - layoutManager.edgeInsets.right) let insetRect = NSRect(x: insetXPos, y: rect.origin.y, width: insetWidth, height: rect.height) for linePosition in layoutManager.lineStorage.linesInRange(range) { From d28d9b0b9ccb0c82d0144445db764ffdcfdccedc Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:57:59 -0600 Subject: [PATCH 6/8] Selection Boxes Use Text Drawing Space --- .../TextSelectionManager/TextSelectionManager+FillRects.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift index f45fff7bf..f0f4d1520 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift @@ -24,7 +24,7 @@ extension TextSelectionManager { var fillRects: [CGRect] = [] - let insetXPos = max(edgeInsets.left, rect.minX) + let insetXPos = max(layoutManager.edgeInsets.left, rect.minX) let insetWidth = max(0, rect.maxX - insetXPos - layoutManager.edgeInsets.right) let insetRect = NSRect(x: insetXPos, y: rect.origin.y, width: insetWidth, height: rect.height) From 251033a2ed318dbd92ae05959d84fbc5e8b3de82 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:27:45 -0600 Subject: [PATCH 7/8] Only Highlight Visible Range --- .../TextSelectionManager+FillRects.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift index f0f4d1520..040ff834e 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift @@ -19,8 +19,10 @@ extension TextSelectionManager { /// - textSelection: The selection to use. /// - Returns: An array of rects that the selection overlaps. func getFillRects(in rect: NSRect, for textSelection: TextSelection) -> [CGRect] { - guard let layoutManager else { return [] } - let range = textSelection.range + guard let layoutManager, + let range = textSelection.range.intersection(textView?.visibleTextRange ?? .zero) else { + return [] + } var fillRects: [CGRect] = [] @@ -33,7 +35,7 @@ extension TextSelectionManager { } // Pixel align these to avoid aliasing on the edges of each rect that should be a solid box. - return fillRects.map { $0.intersection(insetRect).pixelAligned } + return fillRects.map { $0.pixelAligned } } /// Find fill rects for a specific line position. From 3b78fcf286420dbc66d8605bcc63c4d73050e008 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sat, 1 Feb 2025 21:09:27 -0600 Subject: [PATCH 8/8] Mutli-Line Selections Fill Text Drawing Rect --- .../TextLayoutManager/TextLayoutManager.swift | 12 ++++++--- .../TextSelectionManager+FillRects.swift | 27 ++++++++++++------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift index 7e0e56067..1173b81dc 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift @@ -97,10 +97,16 @@ public class TextLayoutManager: NSObject { delegate?.layoutManagerMaxWidthDidChange(newWidth: maxLineWidth + edgeInsets.horizontal) } } - /// The maximum width available to lay out lines in. + + /// The maximum width available to lay out lines in, used to determine how much space is available for laying out + /// lines. Evals to `.greatestFiniteMagnitude` when ``wrapLines`` is `false`. var maxLineLayoutWidth: CGFloat { - wrapLines ? (delegate?.textViewportSize().width ?? .greatestFiniteMagnitude) - edgeInsets.horizontal - : .greatestFiniteMagnitude + wrapLines ? wrapLinesWidth : .greatestFiniteMagnitude + } + + /// The width of the space available to draw text fragments when wrapping lines. + var wrapLinesWidth: CGFloat { + (delegate?.textViewportSize().width ?? .greatestFiniteMagnitude) - edgeInsets.horizontal } /// Contains all data required to perform layout on a text line. diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift index 040ff834e..204e09ebb 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift @@ -9,10 +9,8 @@ import Foundation extension TextSelectionManager { /// Calculate a set of rects for a text selection suitable for filling with the selection color to indicate a - /// multi-line selection. - /// - /// The returned rects are inset by edge insets passed to the text view, the given `rect` parameter can be the 'raw' - /// rect to draw in, no need to inset it before this method call. + /// multi-line selection. The returned rects surround all selected line fragments for the given selection, + /// following the available text layout space, rather than the available selection layout space. /// /// - Parameters: /// - rect: The bounding rect of available draw space. @@ -26,16 +24,27 @@ extension TextSelectionManager { var fillRects: [CGRect] = [] - let insetXPos = max(layoutManager.edgeInsets.left, rect.minX) - let insetWidth = max(0, rect.maxX - insetXPos - layoutManager.edgeInsets.right) - let insetRect = NSRect(x: insetXPos, y: rect.origin.y, width: insetWidth, height: rect.height) + let textWidth = if layoutManager.maxLineLayoutWidth == .greatestFiniteMagnitude { + layoutManager.maxLineWidth + } else { + layoutManager.maxLineLayoutWidth + } + let maxWidth = max(textWidth, layoutManager.wrapLinesWidth) + let validTextDrawingRect = CGRect( + x: layoutManager.edgeInsets.left, + y: rect.minY, + width: maxWidth, + height: rect.height + ).intersection(rect) for linePosition in layoutManager.lineStorage.linesInRange(range) { - fillRects.append(contentsOf: getFillRects(in: insetRect, selectionRange: range, forPosition: linePosition)) + fillRects.append( + contentsOf: getFillRects(in: validTextDrawingRect, selectionRange: range, forPosition: linePosition) + ) } // Pixel align these to avoid aliasing on the edges of each rect that should be a solid box. - return fillRects.map { $0.pixelAligned } + return fillRects.map { $0.intersection(validTextDrawingRect).pixelAligned } } /// Find fill rects for a specific line position.