Skip to content

Commit 726b1ec

Browse files
committed
Move Away from 'drawing' Towards Subclassing LineFragmentView
1 parent a37a6ae commit 726b1ec

File tree

12 files changed

+81
-72
lines changed

12 files changed

+81
-72
lines changed

Sources/CodeEditTextView/Extensions/NSRange+/NSRange+init.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import Foundation
99

10-
extension NSRange {
10+
public extension NSRange {
1111
@inline(__always)
1212
init(start: Int, end: Int) {
1313
self.init(location: start, length: end - start)

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Layout.swift

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ extension TextLayoutManager {
1919
// MARK: - Layout Lines
2020

2121
/// Lays out all visible lines
22-
func layoutLines(in rect: NSRect? = nil) { // swiftlint:disable:this function_body_length
22+
/// - Warning: This is probably not what you're looking for. If you need to invalidate layout, or update lines, this
23+
/// is not the way to do so. This should only be called when macOS performs layout.
24+
public func layoutLines(in rect: NSRect? = nil) { // swiftlint:disable:this function_body_length
2325
assertNotInLayout()
2426
guard let visibleRect = rect ?? delegate?.visibleRect,
2527
!isInTransaction,
@@ -185,11 +187,37 @@ extension TextLayoutManager {
185187
for lineFragment: TextLineStorage<LineFragment>.TextLinePosition,
186188
at yPos: CGFloat
187189
) {
188-
let view = viewReuseQueue.getOrCreateView(forKey: lineFragment.data.id)
190+
let view = viewReuseQueue.getOrCreateView(forKey: lineFragment.data.id) {
191+
renderDelegate?.lineFragmentView(for: lineFragment.data) ?? LineFragmentView()
192+
}
193+
view.translatesAutoresizingMaskIntoConstraints = false
189194
view.setLineFragment(lineFragment.data)
190-
view.renderDelegate = renderDelegate
191195
view.frame.origin = CGPoint(x: edgeInsets.left, y: yPos)
192196
layoutView?.addSubview(view)
193197
view.needsDisplay = true
194198
}
199+
200+
/// Invalidates and prepares a line position for display.
201+
/// - Parameter position: The line position to prepare.
202+
/// - Returns: The height of the newly laid out line and all it's fragments.
203+
package func preparePositionForDisplay(_ position: TextLineStorage<TextLine>.TextLinePosition) -> CGFloat {
204+
guard let textStorage else { return 0 }
205+
let displayData = TextLine.DisplayData(
206+
maxWidth: maxLineLayoutWidth,
207+
lineHeightMultiplier: lineHeightMultiplier,
208+
estimatedLineHeight: estimateLineHeight()
209+
)
210+
position.data.prepareForDisplay(
211+
displayData: displayData,
212+
range: position.range,
213+
stringRef: textStorage,
214+
markedRanges: markedTextManager.markedRanges(in: position.range),
215+
breakStrategy: lineBreakStrategy
216+
)
217+
var height: CGFloat = 0
218+
for fragmentPosition in position.data.lineFragments {
219+
height += fragmentPosition.data.scaledHeight
220+
}
221+
return height
222+
}
195223
}

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+ensureLayout.swift

Lines changed: 0 additions & 34 deletions
This file was deleted.

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,18 +117,20 @@ public class TextLayoutManager: NSObject {
117117
/// - wrapLines: Set to true to wrap lines to the visible editor width.
118118
/// - textView: The view to layout text fragments in.
119119
/// - delegate: A delegate for the layout manager.
120-
init(
120+
public init(
121121
textStorage: NSTextStorage,
122122
lineHeightMultiplier: CGFloat,
123123
wrapLines: Bool,
124124
textView: NSView,
125-
delegate: TextLayoutManagerDelegate?
125+
delegate: TextLayoutManagerDelegate?,
126+
renderDelegate: TextLayoutManagerRenderDelegate? = nil
126127
) {
127128
self.textStorage = textStorage
128129
self.lineHeightMultiplier = lineHeightMultiplier
129130
self.wrapLines = wrapLines
130131
self.layoutView = textView
131132
self.delegate = delegate
133+
self.renderDelegate = renderDelegate
132134
super.init()
133135
prepareTextLines()
134136
}

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManagerRenderDelegate.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ public protocol TextLayoutManagerRenderDelegate: AnyObject {
2020
markedRanges: MarkedRanges?,
2121
breakStrategy: LineBreakStrategy
2222
)
23-
func drawLineFragment(fragment: LineFragment, in context: CGContext)
23+
24+
func lineFragmentView(for lineFragment: LineFragment) -> LineFragmentView
2425
}
2526

26-
extension TextLayoutManagerRenderDelegate {
27+
public extension TextLayoutManagerRenderDelegate {
2728
func prepareForDisplay( // swiftlint:disable:this function_parameter_count
2829
textLine: TextLine,
2930
displayData: TextLine.DisplayData,
@@ -41,7 +42,7 @@ extension TextLayoutManagerRenderDelegate {
4142
)
4243
}
4344

44-
func drawLineFragment(fragment: LineFragment, in context: CGContext) {
45-
fragment.draw(in: context, yPos: 0.0)
45+
func lineFragmentView(for lineFragment: LineFragment) -> LineFragmentView {
46+
LineFragmentView()
4647
}
4748
}

Sources/CodeEditTextView/TextLine/LineFragment.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import CodeEditTextViewObjC
1212
/// fragments, and any lines that need to be broken due to width constraints will contain more than one fragment.
1313
public final class LineFragment: Identifiable, Equatable {
1414
public let id = UUID()
15+
public let documentRange: NSRange
1516
public var ctLine: CTLine
1617
public var width: CGFloat
1718
public var height: CGFloat
@@ -23,13 +24,15 @@ public final class LineFragment: Identifiable, Equatable {
2324
scaledHeight - height
2425
}
2526

26-
public init(
27+
init(
28+
documentRange: NSRange,
2729
ctLine: CTLine,
2830
width: CGFloat,
2931
height: CGFloat,
3032
descent: CGFloat,
3133
lineHeightMultiplier: CGFloat
3234
) {
35+
self.documentRange = documentRange
3336
self.ctLine = ctLine
3437
self.width = width
3538
self.height = height

Sources/CodeEditTextView/TextLine/LineFragmentView.swift

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,44 +8,38 @@
88
import AppKit
99

1010
/// Displays a line fragment.
11-
final class LineFragmentView: NSView {
12-
private weak var lineFragment: LineFragment?
11+
open class LineFragmentView: NSView {
12+
public weak var lineFragment: LineFragment?
1313

14-
weak var renderDelegate: TextLayoutManagerRenderDelegate?
15-
16-
override var isFlipped: Bool {
14+
open override var isFlipped: Bool {
1715
true
1816
}
1917

20-
override var isOpaque: Bool {
18+
open override var isOpaque: Bool {
2119
false
2220
}
2321

24-
override func hitTest(_ point: NSPoint) -> NSView? { nil }
22+
open override func hitTest(_ point: NSPoint) -> NSView? { nil }
2523

2624
/// Prepare the view for reuse, clears the line fragment reference.
27-
override func prepareForReuse() {
25+
open override func prepareForReuse() {
2826
super.prepareForReuse()
2927
lineFragment = nil
3028
}
3129

3230
/// Set a new line fragment for this view, updating view size.
3331
/// - Parameter newFragment: The new fragment to use.
34-
public func setLineFragment(_ newFragment: LineFragment) {
32+
open func setLineFragment(_ newFragment: LineFragment) {
3533
self.lineFragment = newFragment
3634
self.frame.size = CGSize(width: newFragment.width, height: newFragment.scaledHeight)
3735
}
3836

3937
/// Draws the line fragment in the graphics context.
40-
override func draw(_ dirtyRect: NSRect) {
38+
open override func draw(_ dirtyRect: NSRect) {
4139
guard let lineFragment, let context = NSGraphicsContext.current?.cgContext else {
4240
return
4341
}
4442

45-
if let renderDelegate {
46-
renderDelegate.drawLineFragment(fragment: lineFragment, in: context)
47-
} else {
48-
lineFragment.draw(in: context, yPos: 0.0)
49-
}
43+
lineFragment.draw(in: context, yPos: 0.0)
5044
}
5145
}

Sources/CodeEditTextView/TextLine/TextLine.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public final class TextLine: Identifiable, Equatable {
4141
/// - stringRef: A reference to the string storage for the document.
4242
/// - markedRanges: Any marked ranges in the line.
4343
/// - breakStrategy: Determines how line breaks are calculated.
44-
func prepareForDisplay(
44+
public func prepareForDisplay(
4545
displayData: DisplayData,
4646
range: NSRange,
4747
stringRef: NSTextStorage,
@@ -52,6 +52,7 @@ public final class TextLine: Identifiable, Equatable {
5252
self.maxWidth = displayData.maxWidth
5353
typesetter.typeset(
5454
string,
55+
documentRange: range,
5556
displayData: displayData,
5657
breakStrategy: breakStrategy,
5758
markedRanges: markedRanges
@@ -65,8 +66,14 @@ public final class TextLine: Identifiable, Equatable {
6566

6667
/// Contains all required data to perform a typeset and layout operation on a text line.
6768
public struct DisplayData {
68-
let maxWidth: CGFloat
69-
let lineHeightMultiplier: CGFloat
70-
let estimatedLineHeight: CGFloat
69+
public let maxWidth: CGFloat
70+
public let lineHeightMultiplier: CGFloat
71+
public let estimatedLineHeight: CGFloat
72+
73+
public init(maxWidth: CGFloat, lineHeightMultiplier: CGFloat, estimatedLineHeight: CGFloat) {
74+
self.maxWidth = maxWidth
75+
self.lineHeightMultiplier = lineHeightMultiplier
76+
self.estimatedLineHeight = estimatedLineHeight
77+
}
7178
}
7279
}

Sources/CodeEditTextView/TextLine/Typesetter.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import CoreText
1111
final public class Typesetter {
1212
public var typesetter: CTTypesetter?
1313
public var string: NSAttributedString!
14+
public var documentRange: NSRange?
1415
public var lineFragments = TextLineStorage<LineFragment>()
1516

1617
// MARK: - Init & Prepare
@@ -19,10 +20,12 @@ final public class Typesetter {
1920

2021
public func typeset(
2122
_ string: NSAttributedString,
23+
documentRange: NSRange,
2224
displayData: TextLine.DisplayData,
2325
breakStrategy: LineBreakStrategy,
2426
markedRanges: MarkedRanges?
2527
) {
28+
self.documentRange = documentRange
2629
lineFragments.removeAll()
2730
if let markedRanges {
2831
let mutableString = NSMutableAttributedString(attributedString: string)
@@ -62,6 +65,7 @@ final public class Typesetter {
6265
// Insert an empty fragment
6366
let ctLine = CTTypesetterCreateLine(typesetter, CFRangeMake(0, 0))
6467
let fragment = LineFragment(
68+
documentRange: NSRange(location: (documentRange ?? .notFound).location, length: 0),
6569
ctLine: ctLine,
6670
width: 0,
6771
height: estimatedLineHeight/lineHeightMultiplier,
@@ -107,7 +111,9 @@ final public class Typesetter {
107111
var leading: CGFloat = 0
108112
let width = CGFloat(CTLineGetTypographicBounds(ctLine, &ascent, &descent, &leading))
109113
let height = ascent + descent + leading
114+
let range = NSRange(location: (documentRange ?? .notFound).location + range.location, length: range.length)
110115
return LineFragment(
116+
documentRange: range,
111117
ctLine: ctLine,
112118
width: width,
113119
height: height,

Sources/CodeEditTextView/TextView/TextView+Layout.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
import Foundation
99

1010
extension TextView {
11+
override public func layout() {
12+
layoutManager.layoutLines()
13+
super.layout()
14+
}
15+
1116
open override class var isCompatibleWithResponsiveScrolling: Bool {
1217
true
1318
}

0 commit comments

Comments
 (0)