Skip to content

Commit 9b38030

Browse files
committed
Draw InvisibleCharacters From Configuration
1 parent 7f70ee5 commit 9b38030

File tree

10 files changed

+339
-53
lines changed

10 files changed

+339
-53
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// InvisibleCharactersConfig.swift
3+
// CodeEditTextView
4+
//
5+
// Created by Khan Winter on 6/9/25.
6+
//
7+
8+
import Foundation
9+
import AppKit
10+
11+
public enum InvisibleCharacterStyle: Hashable {
12+
case replace(replacementCharacter: String, color: NSColor, font: NSFont)
13+
case emphasize(color: NSColor)
14+
}
15+
16+
public protocol InvisibleCharactersDelegate: AnyObject {
17+
var triggerCharacters: Set<Character> { get }
18+
func invisibleStyle(for character: Character, at range: NSRange, lineRange: NSRange) -> InvisibleCharacterStyle?
19+
}

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Layout.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,9 @@ extension TextLayoutManager {
251251
renderDelegate?.lineFragmentView(for: lineFragment.data) ?? LineFragmentView()
252252
}
253253
view.translatesAutoresizingMaskIntoConstraints = false
254-
view.setLineFragment(lineFragment.data)
254+
view.setLineFragment(lineFragment.data, renderer: lineFragmentRenderer)
255255
view.frame.origin = CGPoint(x: edgeInsets.left, y: yPos)
256-
layoutView?.addSubview(view)
256+
layoutView?.addSubview(view, positioned: .below, relativeTo: nil)
257257
view.needsDisplay = true
258258
}
259259
}

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,21 @@ public class TextLayoutManager: NSObject {
6666

6767
public let attachments: TextAttachmentManager = TextAttachmentManager()
6868

69+
public weak var invisibleCharacterDelegate: InvisibleCharactersDelegate? {
70+
didSet {
71+
lineFragmentRenderer.invisibleCharacterDelegate = invisibleCharacterDelegate
72+
layoutView?.needsDisplay = true
73+
}
74+
}
75+
6976
// MARK: - Internal
7077

7178
weak var textStorage: NSTextStorage?
7279
var lineStorage: TextLineStorage<TextLine> = TextLineStorage()
7380
var markedTextManager: MarkedTextManager = MarkedTextManager()
7481
let viewReuseQueue: ViewReuseQueue<LineFragmentView, LineFragment.ID> = ViewReuseQueue()
82+
let lineFragmentRenderer: LineFragmentRenderer
83+
7584
package var visibleLineIds: Set<TextLine.ID> = []
7685
/// Used to force a complete re-layout using `setNeedsLayout`
7786
package var needsLayout: Bool = false
@@ -122,14 +131,20 @@ public class TextLayoutManager: NSObject {
122131
wrapLines: Bool,
123132
textView: NSView,
124133
delegate: TextLayoutManagerDelegate?,
125-
renderDelegate: TextLayoutManagerRenderDelegate? = nil
134+
renderDelegate: TextLayoutManagerRenderDelegate? = nil,
135+
invisibleCharacterDelegate: InvisibleCharactersDelegate? = nil
126136
) {
127137
self.textStorage = textStorage
128138
self.lineHeightMultiplier = lineHeightMultiplier
129139
self.wrapLines = wrapLines
130140
self.layoutView = textView
131141
self.delegate = delegate
132142
self.renderDelegate = renderDelegate
143+
self.lineFragmentRenderer = LineFragmentRenderer(
144+
textStorage: textStorage,
145+
invisibleCharacterDelegate: invisibleCharacterDelegate
146+
)
147+
self.invisibleCharacterDelegate = invisibleCharacterDelegate
133148
super.init()
134149
prepareTextLines()
135150
attachments.layoutManager = self
@@ -166,6 +181,7 @@ public class TextLayoutManager: NSObject {
166181
viewReuseQueue.usedViews.removeAll()
167182
maxLineWidth = 0
168183
markedTextManager.removeAll()
184+
lineFragmentRenderer.textStorage = textStorage
169185
prepareTextLines()
170186
setNeedsLayout()
171187
}

Sources/CodeEditTextView/TextLine/LineFragment.swift

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public final class LineFragment: Identifiable, Equatable {
4747
}
4848

4949
public let id = UUID()
50+
public let lineRange: NSRange
5051
public let documentRange: NSRange
5152
public var contents: [FragmentContent]
5253
public var width: CGFloat
@@ -60,13 +61,15 @@ public final class LineFragment: Identifiable, Equatable {
6061
}
6162

6263
init(
64+
lineRange: NSRange,
6365
documentRange: NSRange,
6466
contents: [FragmentContent],
6567
width: CGFloat,
6668
height: CGFloat,
6769
descent: CGFloat,
6870
lineHeightMultiplier: CGFloat
6971
) {
72+
self.lineRange = lineRange
7073
self.documentRange = documentRange
7174
self.contents = contents
7275
self.width = width
@@ -102,52 +105,6 @@ public final class LineFragment: Identifiable, Equatable {
102105
}
103106
}
104107

105-
public func draw(in context: CGContext, yPos: CGFloat) {
106-
context.saveGState()
107-
108-
// Removes jagged edges
109-
context.setAllowsAntialiasing(true)
110-
context.setShouldAntialias(true)
111-
112-
// Effectively increases the screen resolution by drawing text in each LED color pixel (R, G, or B), rather than
113-
// the triplet of pixels (RGB) for a regular pixel. This can increase text clarity, but loses effectiveness
114-
// in low-contrast settings.
115-
context.setAllowsFontSubpixelPositioning(true)
116-
context.setShouldSubpixelPositionFonts(true)
117-
118-
// Quantizes the position of each glyph, resulting in slightly less accurate positioning, and gaining higher
119-
// quality bitmaps and performance.
120-
context.setAllowsFontSubpixelQuantization(true)
121-
context.setShouldSubpixelQuantizeFonts(true)
122-
123-
ContextSetHiddenSmoothingStyle(context, 16)
124-
125-
context.textMatrix = .init(scaleX: 1, y: -1)
126-
127-
var currentPosition: CGFloat = 0.0
128-
var currentLocation = 0
129-
for content in contents {
130-
context.saveGState()
131-
switch content.data {
132-
case .text(let ctLine):
133-
context.textPosition = CGPoint(
134-
x: currentPosition,
135-
y: yPos + height - descent + (heightDifference/2)
136-
).pixelAligned
137-
CTLineDraw(ctLine, context)
138-
case .attachment(let attachment):
139-
attachment.attachment.draw(
140-
in: context,
141-
rect: NSRect(x: currentPosition, y: yPos, width: attachment.width, height: scaledHeight)
142-
)
143-
}
144-
context.restoreGState()
145-
currentPosition += content.width
146-
currentLocation += content.length
147-
}
148-
context.restoreGState()
149-
}
150-
151108
package func findContent(at location: Int) -> (content: FragmentContent, position: ContentPosition)? {
152109
var position = ContentPosition(xPos: 0, offset: 0)
153110

0 commit comments

Comments
 (0)