Skip to content

Commit 8ceb3fd

Browse files
Add Text Insets (#65)
1 parent 09a3f21 commit 8ceb3fd

File tree

10 files changed

+218
-126
lines changed

10 files changed

+218
-126
lines changed

Example/CodeEditTextViewExample/CodeEditTextViewExample.xcodeproj/project.pbxproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@
189189
CODE_SIGN_ENTITLEMENTS = CodeEditTextViewExample/CodeEditTextViewExample.entitlements;
190190
CODE_SIGN_STYLE = Automatic;
191191
CURRENT_PROJECT_VERSION = 1;
192-
DEVELOPMENT_ASSET_PATHS = "\"CodeEditTextViewExample/Preview Content\"";
192+
DEVELOPMENT_ASSET_PATHS = "";
193193
DEVELOPMENT_TEAM = "";
194194
ENABLE_PREVIEWS = YES;
195195
GENERATE_INFOPLIST_FILE = YES;
@@ -226,7 +226,7 @@
226226
CODE_SIGN_ENTITLEMENTS = CodeEditTextViewExample/CodeEditTextViewExample.entitlements;
227227
CODE_SIGN_STYLE = Automatic;
228228
CURRENT_PROJECT_VERSION = 1;
229-
DEVELOPMENT_ASSET_PATHS = "\"CodeEditTextViewExample/Preview Content\"";
229+
DEVELOPMENT_ASSET_PATHS = "";
230230
DEVELOPMENT_TEAM = "";
231231
ENABLE_PREVIEWS = YES;
232232
GENERATE_INFOPLIST_FILE = YES;

Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/ContentView.swift

+10-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,18 @@ import SwiftUI
99

1010
struct ContentView: View {
1111
@Binding var document: CodeEditTextViewExampleDocument
12+
@AppStorage("wraplines") private var wrapLines: Bool = true
13+
@AppStorage("edgeinsets") private var enableEdgeInsets: Bool = false
1214

1315
var body: some View {
14-
SwiftUITextView(text: $document.text)
16+
VStack(spacing: 0) {
17+
HStack {
18+
Toggle("Wrap Lines", isOn: $wrapLines)
19+
Toggle("Inset Edges", isOn: $enableEdgeInsets)
20+
}
21+
Divider()
22+
SwiftUITextView(text: $document.text, wrapLines: $wrapLines, enableEdgeInsets: $enableEdgeInsets)
23+
}
1524
}
1625
}
1726

Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/SwiftUITextView.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@ import CodeEditTextView
1111

1212
struct SwiftUITextView: NSViewControllerRepresentable {
1313
@Binding var text: String
14+
@Binding var wrapLines: Bool
15+
@Binding var enableEdgeInsets: Bool
1416

1517
func makeNSViewController(context: Context) -> TextViewController {
1618
let controller = TextViewController(string: text)
1719
context.coordinator.controller = controller
20+
controller.wrapLines = wrapLines
21+
controller.enableEdgeInsets = enableEdgeInsets
1822
return controller
1923
}
2024

2125
func updateNSViewController(_ nsViewController: TextViewController, context: Context) {
22-
// Do nothing, our binding has to be a one-way binding
26+
nsViewController.wrapLines = wrapLines
27+
nsViewController.enableEdgeInsets = enableEdgeInsets
2328
}
2429

2530
func makeCoordinator() -> Coordinator {

Example/CodeEditTextViewExample/CodeEditTextViewExample/Views/TextViewController.swift

+24
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ import CodeEditTextView
1111
class TextViewController: NSViewController {
1212
var scrollView: NSScrollView!
1313
var textView: TextView!
14+
var enableEdgeInsets: Bool = false {
15+
didSet {
16+
if enableEdgeInsets {
17+
textView.edgeInsets = .init(left: 20, right: 30)
18+
textView.textInsets = .init(left: 10, right: 30)
19+
} else {
20+
textView.edgeInsets = .zero
21+
textView.textInsets = .zero
22+
}
23+
}
24+
}
25+
var wrapLines: Bool = true {
26+
didSet {
27+
textView.wrapLines = wrapLines
28+
}
29+
}
1430

1531
init(string: String) {
1632
textView = TextView(string: string)
@@ -24,6 +40,14 @@ class TextViewController: NSViewController {
2440
override func loadView() {
2541
scrollView = NSScrollView()
2642
textView.translatesAutoresizingMaskIntoConstraints = false
43+
textView.wrapLines = wrapLines
44+
if enableEdgeInsets {
45+
textView.edgeInsets = .init(left: 30, right: 30)
46+
textView.textInsets = .init(left: 0, right: 30)
47+
} else {
48+
textView.edgeInsets = .zero
49+
textView.textInsets = .zero
50+
}
2751

2852
scrollView.translatesAutoresizingMaskIntoConstraints = false
2953
scrollView.documentView = textView
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//
2+
// TextSelectionManager+Draw.swift
3+
// CodeEditTextView
4+
//
5+
// Created by Khan Winter on 1/12/25.
6+
//
7+
8+
import AppKit
9+
10+
extension TextSelectionManager {
11+
/// Draws line backgrounds and selection rects for each selection in the given rect.
12+
/// - Parameter rect: The rect to draw in.
13+
func drawSelections(in rect: NSRect) {
14+
guard let context = NSGraphicsContext.current?.cgContext else { return }
15+
context.saveGState()
16+
var highlightedLines: Set<UUID> = []
17+
// For each selection in the rect
18+
for textSelection in textSelections {
19+
if textSelection.range.isEmpty {
20+
drawHighlightedLine(
21+
in: rect,
22+
for: textSelection,
23+
context: context,
24+
highlightedLines: &highlightedLines
25+
)
26+
} else {
27+
drawSelectedRange(in: rect, for: textSelection, context: context)
28+
}
29+
}
30+
context.restoreGState()
31+
}
32+
33+
/// Draws a highlighted line in the given rect.
34+
/// - Parameters:
35+
/// - rect: The rect to draw in.
36+
/// - textSelection: The selection to draw.
37+
/// - context: The context to draw in.
38+
/// - highlightedLines: The set of all lines that have already been highlighted, used to avoid highlighting lines
39+
/// twice and updated if this function comes across a new line id.
40+
private func drawHighlightedLine(
41+
in rect: NSRect,
42+
for textSelection: TextSelection,
43+
context: CGContext,
44+
highlightedLines: inout Set<UUID>
45+
) {
46+
guard let linePosition = layoutManager?.textLineForOffset(textSelection.range.location),
47+
!highlightedLines.contains(linePosition.data.id) else {
48+
return
49+
}
50+
highlightedLines.insert(linePosition.data.id)
51+
context.saveGState()
52+
53+
let insetXPos = max(rect.minX, edgeInsets.left)
54+
let maxWidth = (textView?.frame.width ?? 0) - insetXPos - edgeInsets.right
55+
56+
let selectionRect = CGRect(
57+
x: insetXPos,
58+
y: linePosition.yPos,
59+
width: min(rect.width, maxWidth),
60+
height: linePosition.height
61+
).pixelAligned
62+
63+
if selectionRect.intersects(rect) {
64+
context.setFillColor(selectedLineBackgroundColor.cgColor)
65+
context.fill(selectionRect)
66+
}
67+
context.restoreGState()
68+
}
69+
70+
/// Draws a selected range in the given context.
71+
/// - Parameters:
72+
/// - rect: The rect to draw in.
73+
/// - range: The range to highlight.
74+
/// - context: The context to draw in.
75+
private func drawSelectedRange(in rect: NSRect, for textSelection: TextSelection, context: CGContext) {
76+
context.saveGState()
77+
78+
let fillColor = (textView?.isFirstResponder ?? false)
79+
? selectionBackgroundColor.cgColor
80+
: selectionBackgroundColor.grayscale.cgColor
81+
82+
context.setFillColor(fillColor)
83+
84+
let fillRects = getFillRects(in: rect, for: textSelection)
85+
86+
let minX = fillRects.min(by: { $0.origin.x < $1.origin.x })?.origin.x ?? 0
87+
let minY = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin.y ?? 0
88+
let max = fillRects.max(by: { $0.maxY < $1.maxY }) ?? .zero
89+
let origin = CGPoint(x: minX, y: minY)
90+
let size = CGSize(width: max.maxX - minX, height: max.maxY - minY)
91+
textSelection.boundingRect = CGRect(origin: origin, size: size)
92+
93+
context.fill(fillRects)
94+
context.restoreGState()
95+
}
96+
97+
}

Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager+FillRects.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ extension TextSelectionManager {
3030
return []
3131
}
3232

33-
let insetXPos = max(layoutManager.edgeInsets.left, rect.minX)
34-
let insetWidth = max(0, rect.maxX - insetXPos - layoutManager.edgeInsets.right)
33+
let insetXPos = max(edgeInsets.left, rect.minX)
34+
let insetWidth = max(0, rect.maxX - insetXPos - edgeInsets.right)
3535
let insetRect = NSRect(x: insetXPos, y: rect.origin.y, width: insetWidth, height: rect.height)
3636

3737
// Calculate the first line and any rects selected

Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift

+7-83
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ public class TextSelectionManager: NSObject {
3838
}
3939
}
4040

41+
/// Determines how far inset to draw selection content.
42+
public var edgeInsets: HorizontalEdgeInsets = .zero {
43+
didSet {
44+
delegate?.setNeedsDisplay()
45+
}
46+
}
47+
4148
internal(set) public var textSelections: [TextSelection] = []
4249
weak var layoutManager: TextLayoutManager?
4350
weak var textStorage: NSTextStorage?
@@ -224,87 +231,4 @@ public class TextSelectionManager: NSObject {
224231
textSelection.view?.removeFromSuperview()
225232
}
226233
}
227-
228-
// MARK: - Draw
229-
230-
/// Draws line backgrounds and selection rects for each selection in the given rect.
231-
/// - Parameter rect: The rect to draw in.
232-
func drawSelections(in rect: NSRect) {
233-
guard let context = NSGraphicsContext.current?.cgContext else { return }
234-
context.saveGState()
235-
var highlightedLines: Set<UUID> = []
236-
// For each selection in the rect
237-
for textSelection in textSelections {
238-
if textSelection.range.isEmpty {
239-
drawHighlightedLine(
240-
in: rect,
241-
for: textSelection,
242-
context: context,
243-
highlightedLines: &highlightedLines
244-
)
245-
} else {
246-
drawSelectedRange(in: rect, for: textSelection, context: context)
247-
}
248-
}
249-
context.restoreGState()
250-
}
251-
252-
/// Draws a highlighted line in the given rect.
253-
/// - Parameters:
254-
/// - rect: The rect to draw in.
255-
/// - textSelection: The selection to draw.
256-
/// - context: The context to draw in.
257-
/// - highlightedLines: The set of all lines that have already been highlighted, used to avoid highlighting lines
258-
/// twice and updated if this function comes across a new line id.
259-
private func drawHighlightedLine(
260-
in rect: NSRect,
261-
for textSelection: TextSelection,
262-
context: CGContext,
263-
highlightedLines: inout Set<UUID>
264-
) {
265-
guard let linePosition = layoutManager?.textLineForOffset(textSelection.range.location),
266-
!highlightedLines.contains(linePosition.data.id) else {
267-
return
268-
}
269-
highlightedLines.insert(linePosition.data.id)
270-
context.saveGState()
271-
let selectionRect = CGRect(
272-
x: rect.minX,
273-
y: linePosition.yPos,
274-
width: rect.width,
275-
height: linePosition.height
276-
)
277-
if selectionRect.intersects(rect) {
278-
context.setFillColor(selectedLineBackgroundColor.cgColor)
279-
context.fill(selectionRect)
280-
}
281-
context.restoreGState()
282-
}
283-
284-
/// Draws a selected range in the given context.
285-
/// - Parameters:
286-
/// - rect: The rect to draw in.
287-
/// - range: The range to highlight.
288-
/// - context: The context to draw in.
289-
private func drawSelectedRange(in rect: NSRect, for textSelection: TextSelection, context: CGContext) {
290-
context.saveGState()
291-
292-
let fillColor = (textView?.isFirstResponder ?? false)
293-
? selectionBackgroundColor.cgColor
294-
: selectionBackgroundColor.grayscale.cgColor
295-
296-
context.setFillColor(fillColor)
297-
298-
let fillRects = getFillRects(in: rect, for: textSelection)
299-
300-
let minX = fillRects.min(by: { $0.origin.x < $1.origin.x })?.origin.x ?? 0
301-
let minY = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin.y ?? 0
302-
let max = fillRects.max(by: { $0.maxY < $1.maxY }) ?? .zero
303-
let origin = CGPoint(x: minX, y: minY)
304-
let size = CGSize(width: max.maxX - minX, height: max.maxY - minY)
305-
textSelection.boundingRect = CGRect(origin: origin, size: size)
306-
307-
context.fill(fillRects)
308-
context.restoreGState()
309-
}
310234
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// TextView+SetText.swift
3+
// CodeEditTextView
4+
//
5+
// Created by Khan Winter on 1/12/25.
6+
//
7+
8+
import AppKit
9+
10+
extension TextView {
11+
/// Sets the text view's text to a new value.
12+
/// - Parameter text: The new contents of the text view.
13+
public func setText(_ text: String) {
14+
let newStorage = NSTextStorage(string: text)
15+
self.setTextStorage(newStorage)
16+
}
17+
18+
/// Set a new text storage object for the view.
19+
/// - Parameter textStorage: The new text storage to use.
20+
public func setTextStorage(_ textStorage: NSTextStorage) {
21+
self.textStorage = textStorage
22+
23+
subviews.forEach { view in
24+
view.removeFromSuperview()
25+
}
26+
27+
textStorage.addAttributes(typingAttributes, range: documentRange)
28+
layoutManager.textStorage = textStorage
29+
layoutManager.reset()
30+
31+
selectionManager.textStorage = textStorage
32+
selectionManager.setSelectedRanges(selectionManager.textSelections.map { $0.range })
33+
34+
_undoManager?.clearStack()
35+
36+
textStorage.delegate = storageDelegate
37+
needsDisplay = true
38+
needsLayout = true
39+
}
40+
}

0 commit comments

Comments
 (0)