diff --git a/Package.swift b/Package.swift index cb4b82a2..da4046e5 100644 --- a/Package.swift +++ b/Package.swift @@ -36,13 +36,20 @@ let package = Package( name: "CodeEditTextView", dependencies: [ "TextStory", - .product(name: "Collections", package: "swift-collections") + .product(name: "Collections", package: "swift-collections"), + "CodeEditTextViewObjC" ], plugins: [ .plugin(name: "SwiftLint", package: "SwiftLintPlugin") ] ), + // ObjC addons + .target( + name: "CodeEditTextViewObjC", + publicHeadersPath: "include" + ), + // Tests for the text view .testTarget( name: "CodeEditTextViewTests", diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift index 400d013a..0cc69376 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift @@ -230,7 +230,12 @@ public class TextLayoutManager: NSObject { /// Lays out all visible lines func layoutLines() { // swiftlint:disable:this function_body_length - guard let visibleRect = delegate?.visibleRect, !isInTransaction, let textStorage else { return } + guard layoutView?.superview != nil, + let visibleRect = delegate?.visibleRect, + !isInTransaction, + let textStorage else { + return + } CATransaction.begin() let minY = max(visibleRect.minY, 0) let maxY = max(visibleRect.maxY, 0) diff --git a/Sources/CodeEditTextView/TextLine/LineFragmentView.swift b/Sources/CodeEditTextView/TextLine/LineFragmentView.swift index e971de57..485b303d 100644 --- a/Sources/CodeEditTextView/TextLine/LineFragmentView.swift +++ b/Sources/CodeEditTextView/TextLine/LineFragmentView.swift @@ -6,6 +6,7 @@ // import AppKit +import CodeEditTextViewObjC /// Displays a line fragment. final class LineFragmentView: NSView { @@ -23,7 +24,6 @@ final class LineFragmentView: NSView { override func prepareForReuse() { super.prepareForReuse() lineFragment = nil - } /// Set a new line fragment for this view, updating view size. @@ -39,13 +39,24 @@ final class LineFragmentView: NSView { return } context.saveGState() - context.setAllowsFontSmoothing(true) - context.setShouldSmoothFonts(true) + + context.setAllowsAntialiasing(true) + context.setShouldAntialias(true) + context.setAllowsFontSmoothing(false) + context.setShouldSmoothFonts(false) + context.setAllowsFontSubpixelPositioning(true) + context.setShouldSubpixelPositionFonts(true) + context.setAllowsFontSubpixelQuantization(true) + context.setShouldSubpixelQuantizeFonts(true) + + ContextSetHiddenSmoothingStyle(context, 16) + context.textMatrix = .init(scaleX: 1, y: -1) context.textPosition = CGPoint( x: 0, y: lineFragment.height - lineFragment.descent + (lineFragment.heightDifference/2) ).pixelAligned + CTLineDraw(lineFragment.ctLine, context) context.restoreGState() } diff --git a/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift b/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift index 64eab098..333f41a2 100644 --- a/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift +++ b/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift @@ -20,10 +20,17 @@ extension TextView { NotificationCenter.default.post(name: Self.textWillChangeNotification, object: self) layoutManager.beginTransaction() textStorage.beginEditing() - // Can't insert an ssempty string into an empty range. One must be not empty + + var shouldEndGrouping = false + if !(_undoManager?.isGrouping ?? false) { + _undoManager?.beginGrouping() + shouldEndGrouping = true + } + + // Can't insert an empty string into an empty range. One must be not empty for range in ranges.sorted(by: { $0.location > $1.location }) where - (delegate?.textView(self, shouldReplaceContentsIn: range, with: string) ?? true) - && (!range.isEmpty || !string.isEmpty) { + (!range.isEmpty || !string.isEmpty) && + (delegate?.textView(self, shouldReplaceContentsIn: range, with: string) ?? true) { delegate?.textView(self, willReplaceContentsIn: range, with: string) layoutManager.willReplaceCharactersInRange(range: range, with: string) @@ -38,6 +45,11 @@ extension TextView { delegate?.textView(self, didReplaceContentsIn: range, with: string) } + + if shouldEndGrouping { + _undoManager?.endGrouping() + } + layoutManager.endTransaction() textStorage.endEditing() selectionManager.notifyAfterEdit() diff --git a/Sources/CodeEditTextView/TextView/TextView.swift b/Sources/CodeEditTextView/TextView/TextView.swift index 90285fd3..dc34b96c 100644 --- a/Sources/CodeEditTextView/TextView/TextView.swift +++ b/Sources/CodeEditTextView/TextView/TextView.swift @@ -203,7 +203,7 @@ public class TextView: NSView, NSTextContent { (" " as NSString).size(withAttributes: [.font: font]).width } - var _undoManager: CEUndoManager? + internal(set) public var _undoManager: CEUndoManager? @objc dynamic open var allowsUndo: Bool var scrollView: NSScrollView? { diff --git a/Sources/CodeEditTextView/Utils/CEUndoManager.swift b/Sources/CodeEditTextView/Utils/CEUndoManager.swift index 4fac1720..86eca088 100644 --- a/Sources/CodeEditTextView/Utils/CEUndoManager.swift +++ b/Sources/CodeEditTextView/Utils/CEUndoManager.swift @@ -96,11 +96,11 @@ public class CEUndoManager { return } isUndoing = true + NotificationCenter.default.post(name: .NSUndoManagerWillUndoChange, object: self.manager) for mutation in item.mutations.reversed() { - NotificationCenter.default.post(name: .NSUndoManagerWillUndoChange, object: self.manager) - textView.insertText(mutation.inverse.string, replacementRange: mutation.inverse.range) - NotificationCenter.default.post(name: .NSUndoManagerDidUndoChange, object: self.manager) + textView.replaceCharacters(in: mutation.inverse.range, with: mutation.inverse.string) } + NotificationCenter.default.post(name: .NSUndoManagerDidUndoChange, object: self.manager) redoStack.append(item) isUndoing = false } @@ -111,11 +111,11 @@ public class CEUndoManager { return } isRedoing = true + NotificationCenter.default.post(name: .NSUndoManagerWillRedoChange, object: self.manager) for mutation in item.mutations { - NotificationCenter.default.post(name: .NSUndoManagerWillRedoChange, object: self.manager) - textView.insertText(mutation.mutation.string, replacementRange: mutation.mutation.range) - NotificationCenter.default.post(name: .NSUndoManagerDidRedoChange, object: self.manager) + textView.replaceCharacters(in: mutation.mutation.range, with: mutation.mutation.string) } + NotificationCenter.default.post(name: .NSUndoManagerDidRedoChange, object: self.manager) undoStack.append(item) isRedoing = false } @@ -159,11 +159,19 @@ public class CEUndoManager { /// Groups all incoming mutations. public func beginGrouping() { + guard !isGrouping else { + assertionFailure("UndoManager already in a group. Call `endGrouping` before this can be called.") + return + } isGrouping = true } /// Stops grouping all incoming mutations. public func endGrouping() { + guard isGrouping else { + assertionFailure("UndoManager not in a group. Call `beginGrouping` before this can be called.") + return + } isGrouping = false } diff --git a/Sources/CodeEditTextViewObjC/CGContextHidden.m b/Sources/CodeEditTextViewObjC/CGContextHidden.m new file mode 100644 index 00000000..a5318ea6 --- /dev/null +++ b/Sources/CodeEditTextViewObjC/CGContextHidden.m @@ -0,0 +1,15 @@ +// +// CGContextHidden.m +// CodeEditTextViewObjC +// +// Created by Khan Winter on 2/12/24. +// + +#import +#import "CGContextHidden.h" + +extern void CGContextSetFontSmoothingStyle(CGContextRef, int); + +void ContextSetHiddenSmoothingStyle(CGContextRef context, int style) { + CGContextSetFontSmoothingStyle(context, style); +} diff --git a/Sources/CodeEditTextViewObjC/include/CGContextHidden.h b/Sources/CodeEditTextViewObjC/include/CGContextHidden.h new file mode 100644 index 00000000..87a5aad1 --- /dev/null +++ b/Sources/CodeEditTextViewObjC/include/CGContextHidden.h @@ -0,0 +1,15 @@ +// +// CGContextHidden.h +// CodeEditTextViewObjC +// +// Created by Khan Winter on 2/12/24. +// + +#ifndef CGContextHidden_h +#define CGContextHidden_h + +#import + +void ContextSetHiddenSmoothingStyle(CGContextRef context, int style); + +#endif /* CGContextHidden_h */ diff --git a/Sources/CodeEditTextViewObjC/include/module.modulemap b/Sources/CodeEditTextViewObjC/include/module.modulemap new file mode 100644 index 00000000..8b431cb1 --- /dev/null +++ b/Sources/CodeEditTextViewObjC/include/module.modulemap @@ -0,0 +1,3 @@ +module CodeEditTextViewObjC { + header "CGContextHidden.h" +}