Skip to content

Commit e4d4853

Browse files
Reduce Text Artifacts, Fix Layout Bug, Public Undo Manager (#23)
### Description - Reduces text drawing artifacts by turning off font smoothing, enabling subpixel positioning and font quantization, and a hidden smoothing API. - Adds an internal ObjC target to accomplish the previous point. - Fixes a layout bug where layout bounds would be nearly infinite due to the view being told to lay out but not be in the view hierarchy yet, causing a hang and memory explosion as every line in a potentially large document is laid out and rendered. - Fixes a small bug with the undo manager's grouping behavior and makes it public (for a fix in CESE for undo-redo related bugs), as well as reordering some notifications in the undo manager. ### Related Issues * N/A ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots Before: <img width="314" alt="Screenshot 2024-02-12 at 2 03 35 PM" src="https://github.com/CodeEditApp/CodeEditTextView/assets/35942988/b4f3b3ae-0eb7-4e0b-bde8-df0b7c8fcc65"> After (left CE, right Xcode): ![Screenshot 2024-02-13 at 2 23 36 PM](https://github.com/CodeEditApp/CodeEditTextView/assets/35942988/39d5fdce-cc3e-4dfd-94ea-4f571c6f3c27)
1 parent f2f9d93 commit e4d4853

File tree

9 files changed

+91
-15
lines changed

9 files changed

+91
-15
lines changed

Package.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,20 @@ let package = Package(
3636
name: "CodeEditTextView",
3737
dependencies: [
3838
"TextStory",
39-
.product(name: "Collections", package: "swift-collections")
39+
.product(name: "Collections", package: "swift-collections"),
40+
"CodeEditTextViewObjC"
4041
],
4142
plugins: [
4243
.plugin(name: "SwiftLint", package: "SwiftLintPlugin")
4344
]
4445
),
4546

47+
// ObjC addons
48+
.target(
49+
name: "CodeEditTextViewObjC",
50+
publicHeadersPath: "include"
51+
),
52+
4653
// Tests for the text view
4754
.testTarget(
4855
name: "CodeEditTextViewTests",

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,12 @@ public class TextLayoutManager: NSObject {
230230

231231
/// Lays out all visible lines
232232
func layoutLines() { // swiftlint:disable:this function_body_length
233-
guard let visibleRect = delegate?.visibleRect, !isInTransaction, let textStorage else { return }
233+
guard layoutView?.superview != nil,
234+
let visibleRect = delegate?.visibleRect,
235+
!isInTransaction,
236+
let textStorage else {
237+
return
238+
}
234239
CATransaction.begin()
235240
let minY = max(visibleRect.minY, 0)
236241
let maxY = max(visibleRect.maxY, 0)

Sources/CodeEditTextView/TextLine/LineFragmentView.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import AppKit
9+
import CodeEditTextViewObjC
910

1011
/// Displays a line fragment.
1112
final class LineFragmentView: NSView {
@@ -23,7 +24,6 @@ final class LineFragmentView: NSView {
2324
override func prepareForReuse() {
2425
super.prepareForReuse()
2526
lineFragment = nil
26-
2727
}
2828

2929
/// Set a new line fragment for this view, updating view size.
@@ -39,13 +39,24 @@ final class LineFragmentView: NSView {
3939
return
4040
}
4141
context.saveGState()
42-
context.setAllowsFontSmoothing(true)
43-
context.setShouldSmoothFonts(true)
42+
43+
context.setAllowsAntialiasing(true)
44+
context.setShouldAntialias(true)
45+
context.setAllowsFontSmoothing(false)
46+
context.setShouldSmoothFonts(false)
47+
context.setAllowsFontSubpixelPositioning(true)
48+
context.setShouldSubpixelPositionFonts(true)
49+
context.setAllowsFontSubpixelQuantization(true)
50+
context.setShouldSubpixelQuantizeFonts(true)
51+
52+
ContextSetHiddenSmoothingStyle(context, 16)
53+
4454
context.textMatrix = .init(scaleX: 1, y: -1)
4555
context.textPosition = CGPoint(
4656
x: 0,
4757
y: lineFragment.height - lineFragment.descent + (lineFragment.heightDifference/2)
4858
).pixelAligned
59+
4960
CTLineDraw(lineFragment.ctLine, context)
5061
context.restoreGState()
5162
}

Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,17 @@ extension TextView {
2020
NotificationCenter.default.post(name: Self.textWillChangeNotification, object: self)
2121
layoutManager.beginTransaction()
2222
textStorage.beginEditing()
23-
// Can't insert an ssempty string into an empty range. One must be not empty
23+
24+
var shouldEndGrouping = false
25+
if !(_undoManager?.isGrouping ?? false) {
26+
_undoManager?.beginGrouping()
27+
shouldEndGrouping = true
28+
}
29+
30+
// Can't insert an empty string into an empty range. One must be not empty
2431
for range in ranges.sorted(by: { $0.location > $1.location }) where
25-
(delegate?.textView(self, shouldReplaceContentsIn: range, with: string) ?? true)
26-
&& (!range.isEmpty || !string.isEmpty) {
32+
(!range.isEmpty || !string.isEmpty) &&
33+
(delegate?.textView(self, shouldReplaceContentsIn: range, with: string) ?? true) {
2734
delegate?.textView(self, willReplaceContentsIn: range, with: string)
2835

2936
layoutManager.willReplaceCharactersInRange(range: range, with: string)
@@ -38,6 +45,11 @@ extension TextView {
3845

3946
delegate?.textView(self, didReplaceContentsIn: range, with: string)
4047
}
48+
49+
if shouldEndGrouping {
50+
_undoManager?.endGrouping()
51+
}
52+
4153
layoutManager.endTransaction()
4254
textStorage.endEditing()
4355
selectionManager.notifyAfterEdit()

Sources/CodeEditTextView/TextView/TextView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ public class TextView: NSView, NSTextContent {
203203
(" " as NSString).size(withAttributes: [.font: font]).width
204204
}
205205

206-
var _undoManager: CEUndoManager?
206+
internal(set) public var _undoManager: CEUndoManager?
207207
@objc dynamic open var allowsUndo: Bool
208208

209209
var scrollView: NSScrollView? {

Sources/CodeEditTextView/Utils/CEUndoManager.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,11 @@ public class CEUndoManager {
9696
return
9797
}
9898
isUndoing = true
99+
NotificationCenter.default.post(name: .NSUndoManagerWillUndoChange, object: self.manager)
99100
for mutation in item.mutations.reversed() {
100-
NotificationCenter.default.post(name: .NSUndoManagerWillUndoChange, object: self.manager)
101-
textView.insertText(mutation.inverse.string, replacementRange: mutation.inverse.range)
102-
NotificationCenter.default.post(name: .NSUndoManagerDidUndoChange, object: self.manager)
101+
textView.replaceCharacters(in: mutation.inverse.range, with: mutation.inverse.string)
103102
}
103+
NotificationCenter.default.post(name: .NSUndoManagerDidUndoChange, object: self.manager)
104104
redoStack.append(item)
105105
isUndoing = false
106106
}
@@ -111,11 +111,11 @@ public class CEUndoManager {
111111
return
112112
}
113113
isRedoing = true
114+
NotificationCenter.default.post(name: .NSUndoManagerWillRedoChange, object: self.manager)
114115
for mutation in item.mutations {
115-
NotificationCenter.default.post(name: .NSUndoManagerWillRedoChange, object: self.manager)
116-
textView.insertText(mutation.mutation.string, replacementRange: mutation.mutation.range)
117-
NotificationCenter.default.post(name: .NSUndoManagerDidRedoChange, object: self.manager)
116+
textView.replaceCharacters(in: mutation.mutation.range, with: mutation.mutation.string)
118117
}
118+
NotificationCenter.default.post(name: .NSUndoManagerDidRedoChange, object: self.manager)
119119
undoStack.append(item)
120120
isRedoing = false
121121
}
@@ -159,11 +159,19 @@ public class CEUndoManager {
159159

160160
/// Groups all incoming mutations.
161161
public func beginGrouping() {
162+
guard !isGrouping else {
163+
assertionFailure("UndoManager already in a group. Call `endGrouping` before this can be called.")
164+
return
165+
}
162166
isGrouping = true
163167
}
164168

165169
/// Stops grouping all incoming mutations.
166170
public func endGrouping() {
171+
guard isGrouping else {
172+
assertionFailure("UndoManager not in a group. Call `beginGrouping` before this can be called.")
173+
return
174+
}
167175
isGrouping = false
168176
}
169177

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// CGContextHidden.m
3+
// CodeEditTextViewObjC
4+
//
5+
// Created by Khan Winter on 2/12/24.
6+
//
7+
8+
#import <Cocoa/Cocoa.h>
9+
#import "CGContextHidden.h"
10+
11+
extern void CGContextSetFontSmoothingStyle(CGContextRef, int);
12+
13+
void ContextSetHiddenSmoothingStyle(CGContextRef context, int style) {
14+
CGContextSetFontSmoothingStyle(context, style);
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// CGContextHidden.h
3+
// CodeEditTextViewObjC
4+
//
5+
// Created by Khan Winter on 2/12/24.
6+
//
7+
8+
#ifndef CGContextHidden_h
9+
#define CGContextHidden_h
10+
11+
#import <Cocoa/Cocoa.h>
12+
13+
void ContextSetHiddenSmoothingStyle(CGContextRef context, int style);
14+
15+
#endif /* CGContextHidden_h */
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module CodeEditTextViewObjC {
2+
header "CGContextHidden.h"
3+
}

0 commit comments

Comments
 (0)