Skip to content

Commit 878565e

Browse files
committed
Workaround FB15131180 - extra line fragment wrong frame
1 parent a7b54a3 commit 878565e

6 files changed

+357
-93
lines changed

Sources/STTextViewAppKit/STTextView+Gutter.swift

+64-14
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,11 @@ extension STTextView {
124124
textLayoutManager.enumerateTextLayoutFragments(in: viewportRange, options: .ensuresLayout) { layoutFragment in
125125
let contentRangeInElement = (layoutFragment.textElement as? NSTextParagraph)?.paragraphContentRange ?? layoutFragment.rangeInElement
126126

127-
for lineFragment in layoutFragment.textLineFragments where (lineFragment.isExtraLineFragment || layoutFragment.textLineFragments.first == lineFragment) {
128-
127+
for textLineFragment in layoutFragment.textLineFragments where (textLineFragment.isExtraLineFragment || layoutFragment.textLineFragments.first == textLineFragment) {
129128
func isLineSelected() -> Bool {
130129
textLayoutManager.textSelections.flatMap(\.textRanges).reduce(true) { partialResult, selectionTextRange in
131130
var result = true
132-
if lineFragment.isExtraLineFragment {
131+
if textLineFragment.isExtraLineFragment {
133132
let c1 = layoutFragment.rangeInElement.endLocation == selectionTextRange.location
134133
result = result && c1
135134
} else {
@@ -145,22 +144,73 @@ extension STTextView {
145144
}
146145

147146
let isLineSelected = isLineSelected()
147+
let lineNumber = startLineIndex + linesCount + 1
148148

149+
// calculated values depends on the "isExtraLineFragment" condition
149150
var baselineYOffset: CGFloat = 0
150-
if let paragraphStyle = lineFragment.attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle, !paragraphStyle.lineHeightMultiple.isAlmostZero() {
151-
baselineYOffset = -(lineFragment.typographicBounds.height * (paragraphStyle.lineHeightMultiple - 1.0) / 2)
152-
}
151+
let locationForFirstCharacter: CGPoint
152+
let lineFragmentFrame: CGRect
153+
154+
// The logic for extra line handling would use some cleanup
155+
// It apply workaround for FB15131180 invalid frame being reported
156+
// for the extra line fragment. The workaround is to calculate (adjust)
157+
// extra line fragment frame based on previous text line (from the same layout fragment)
158+
if layoutFragment.isExtraLineFragment {
159+
if !textLineFragment.isExtraLineFragment {
160+
locationForFirstCharacter = textLineFragment.locationForCharacter(at: 0)
161+
162+
if let paragraphStyle = textLineFragment.attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle, !paragraphStyle.lineHeightMultiple.isAlmostZero() {
163+
baselineYOffset = -(textLineFragment.typographicBounds.height * (paragraphStyle.lineHeightMultiple - 1.0) / 2)
164+
}
153165

154-
let lineNumber = startLineIndex + linesCount + 1
155-
let locationForFirstCharacter = lineFragment.locationForCharacter(at: 0)
166+
lineFragmentFrame = CGRect(
167+
origin: CGPoint(
168+
x: layoutFragment.layoutFragmentFrame.origin.x + textLineFragment.typographicBounds.origin.x,
169+
y: layoutFragment.layoutFragmentFrame.origin.y + textLineFragment.typographicBounds.origin.y - scrollView.contentView.bounds.minY/*contentOffset.y*/
170+
),
171+
size: CGSize(
172+
width: textLineFragment.typographicBounds.width,
173+
height: textLineFragment.typographicBounds.height
174+
)
175+
)
176+
} else {
177+
// Use values from the same layoutFragment but previous line, that is not extra line fragment.
178+
// Since this is extra line fragment, it is guaranteed that there is at least 2 line fragments in the layout fragment
179+
let prevTextLineFragment = layoutFragment.textLineFragments[layoutFragment.textLineFragments.count - 2]
180+
locationForFirstCharacter = prevTextLineFragment.locationForCharacter(at: 0)
181+
182+
if let paragraphStyle = prevTextLineFragment.attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle, !paragraphStyle.lineHeightMultiple.isAlmostZero() {
183+
baselineYOffset = -(prevTextLineFragment.typographicBounds.height * (paragraphStyle.lineHeightMultiple - 1.0) / 2)
184+
}
156185

157-
var lineFragmentFrame = CGRect(origin: CGPoint(x: 0, y: layoutFragment.layoutFragmentFrame.origin.y - scrollView.contentView.bounds.minY/*contentOffset.y*/), size: layoutFragment.layoutFragmentFrame.size)
186+
lineFragmentFrame = CGRect(
187+
origin: CGPoint(
188+
x: layoutFragment.layoutFragmentFrame.origin.x + prevTextLineFragment.typographicBounds.origin.x,
189+
y: layoutFragment.layoutFragmentFrame.origin.y + prevTextLineFragment.typographicBounds.maxY - scrollView.contentView.bounds.minY/*contentOffset.y*/
190+
),
191+
size: CGSize(
192+
width: textLineFragment.typographicBounds.width,
193+
height: prevTextLineFragment.typographicBounds.height
194+
)
195+
)
196+
}
197+
} else {
198+
locationForFirstCharacter = textLineFragment.locationForCharacter(at: 0)
158199

159-
lineFragmentFrame.origin.y += lineFragment.typographicBounds.origin.y
160-
if lineFragment.isExtraLineFragment {
161-
lineFragmentFrame.size.height = lineFragment.typographicBounds.height
162-
} else if !lineFragment.isExtraLineFragment, let extraLineFragment = layoutFragment.textLineFragments.first(where: { $0.isExtraLineFragment }) {
163-
lineFragmentFrame.size.height -= extraLineFragment.typographicBounds.height
200+
if let paragraphStyle = textLineFragment.attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle, !paragraphStyle.lineHeightMultiple.isAlmostZero() {
201+
baselineYOffset = -(textLineFragment.typographicBounds.height * (paragraphStyle.lineHeightMultiple - 1.0) / 2)
202+
}
203+
204+
lineFragmentFrame = CGRect(
205+
origin: CGPoint(
206+
x: layoutFragment.layoutFragmentFrame.origin.x + textLineFragment.typographicBounds.origin.x,
207+
y: layoutFragment.layoutFragmentFrame.origin.y + textLineFragment.typographicBounds.origin.y - scrollView.contentView.bounds.minY/*contentOffset.y*/
208+
),
209+
size: CGSize(
210+
width: layoutFragment.layoutFragmentFrame.width, // extend width to he fragment layout for the convenience of gutter
211+
height: layoutFragment.layoutFragmentFrame.height
212+
)
213+
)
164214
}
165215

166216
var effectiveLineTextAttributes = lineTextAttributes

Sources/STTextViewAppKit/STTextView+InsertionPoint.swift

+72-16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import Foundation
55
import AppKit
6+
import STTextKitPlus
67

78
extension STTextView {
89

@@ -15,28 +16,83 @@ extension STTextView {
1516
return
1617
}
1718

18-
let textSelectionFrames = insertionPointsRanges.compactMap { textRange -> CGRect? in
19-
20-
guard let textSegmentFrame = textLayoutManager.textSegmentFrame(in: textRange, type: .selection, options: .rangeNotRequired) else {
21-
return nil
22-
}
23-
24-
let selectionFrame = textSegmentFrame.intersection(frame)
25-
26-
// because `textLayoutManager.enumerateTextLayoutFragments(from: nil, options: [.ensuresExtraLineFragment, .ensuresLayout, .estimatesSize])`
27-
// returns unexpected value for extra line fragment height (return 14) that is not correct in the context,
28-
// therefore for empty override height with value manually calculated from font + paragraph style
29-
if textRange == textLayoutManager.documentRange, textRange.isEmpty {
30-
return CGRect(origin: selectionFrame.origin, size: CGSize(width: selectionFrame.width, height: typingLineHeight)).pixelAligned
19+
// rewrite it to lines
20+
var textSelectionFrames: [CGRect] = []
21+
for selectionTextRange in insertionPointsRanges {
22+
textLayoutManager.enumerateTextSegments(in: selectionTextRange, type: .standard) { textSegmentRange, textSegmentFrame, baselinePosition, textContainer in
23+
if let textSegmentRange {
24+
let documentRange = textLayoutManager.documentRange
25+
guard !documentRange.isEmpty else {
26+
// empty document
27+
textSelectionFrames.append(
28+
CGRect(
29+
origin: CGPoint(
30+
x: textSegmentFrame.origin.x,
31+
y: textSegmentFrame.origin.y
32+
),
33+
size: CGSize(
34+
width: textSegmentFrame.width,
35+
height: typingLineHeight
36+
)
37+
)
38+
)
39+
return false
40+
}
41+
42+
let isAtEndLocation = textSegmentRange.location == documentRange.endLocation
43+
guard !isAtEndLocation else {
44+
// At the end of non-empty document
45+
46+
// FB15131180: extra line fragment frame is not correct hence workaround location and height at extra line
47+
if let layoutFragment = textLayoutManager.extraLineTextLayoutFragment() {
48+
// at least 2 lines guaranteed at this point
49+
let prevTextLineFragment = layoutFragment.textLineFragments[layoutFragment.textLineFragments.count - 2]
50+
textSelectionFrames.append(
51+
CGRect(
52+
origin: CGPoint(
53+
x: textSegmentFrame.origin.x,
54+
y: layoutFragment.layoutFragmentFrame.origin.y + prevTextLineFragment.typographicBounds.maxY
55+
),
56+
size: CGSize(
57+
width: textSegmentFrame.width,
58+
height: prevTextLineFragment.typographicBounds.height
59+
)
60+
)
61+
)
62+
} else if let prevLocation = textLayoutManager.location(textSegmentRange.endLocation, offsetBy: -1),
63+
let prevTextLineFragment = textLayoutManager.textLineFragment(at: prevLocation)
64+
{
65+
// Get insertion point height from the last-to-end (last) line fragment location
66+
// since we're at the end location at this point.
67+
textSelectionFrames.append(
68+
CGRect(
69+
origin: CGPoint(
70+
x: textSegmentFrame.origin.x,
71+
y: textSegmentFrame.origin.y
72+
),
73+
size: CGSize(
74+
width: textSegmentFrame.width,
75+
height: prevTextLineFragment.typographicBounds.height
76+
)
77+
)
78+
)
79+
}
80+
return false
81+
}
82+
83+
// Regular where segment frame is correct
84+
textSelectionFrames.append(
85+
textSegmentFrame
86+
)
87+
}
88+
return true
3189
}
32-
33-
return selectionFrame
3490
}
3591

3692
removeInsertionPointView()
3793

3894
for selectionFrame in textSelectionFrames where !selectionFrame.isNull && !selectionFrame.isInfinite {
39-
let insertionViewFrame = CGRect(origin: selectionFrame.origin, size: CGSize(width: max(2, selectionFrame.width), height: selectionFrame.height))
95+
let insertionViewFrame = CGRect(origin: selectionFrame.origin, size: CGSize(width: max(2, selectionFrame.width), height: selectionFrame.height)).pixelAligned
4096

4197
var textInsertionIndicator: any STInsertionPointIndicatorProtocol
4298
if let customTextInsertionIndicator = self.delegateProxy.textViewInsertionPointView(self, frame: CGRect(origin: .zero, size: insertionViewFrame.size)) {

Sources/STTextViewAppKit/STTextView.swift

+42-21
Original file line numberDiff line numberDiff line change
@@ -977,12 +977,12 @@ import AVFoundation
977977
// extra line fragment area (sic).
978978
textLayoutManager.enumerateTextLayoutFragments(in: viewportRange) { layoutFragment in
979979
let contentRangeInElement = (layoutFragment.textElement as? NSTextParagraph)?.paragraphContentRange ?? layoutFragment.rangeInElement
980-
for lineFragment in layoutFragment.textLineFragments {
981-
980+
for textLineFragment in layoutFragment.textLineFragments {
981+
982982
func isLineSelected() -> Bool {
983983
textLayoutManager.textSelections.flatMap(\.textRanges).reduce(true) { partialResult, selectionTextRange in
984984
var result = true
985-
if lineFragment.isExtraLineFragment {
985+
if textLineFragment.isExtraLineFragment {
986986
let c1 = layoutFragment.rangeInElement.endLocation == selectionTextRange.location
987987
result = result && c1
988988
} else {
@@ -996,27 +996,48 @@ import AVFoundation
996996
return partialResult && result
997997
}
998998
}
999-
1000-
if isLineSelected() {
1001-
var lineFragmentFrame = layoutFragment.layoutFragmentFrame
1002-
lineFragmentFrame.size.height = lineFragment.typographicBounds.height
1003-
1004-
1005-
let r = CGRect(
1006-
origin: CGPoint(
1007-
x: selectionView.bounds.minX,
1008-
y: lineFragmentFrame.origin.y + lineFragment.typographicBounds.minY
1009-
),
1010-
size: CGSize(
1011-
width: selectionView.bounds.width,
1012-
height: lineFragmentFrame.height
999+
1000+
let isLineSelected = isLineSelected()
1001+
1002+
if isLineSelected {
1003+
let lineSelectionRectangle: CGRect
1004+
1005+
if !textLineFragment.isExtraLineFragment {
1006+
var lineFragmentFrame = layoutFragment.layoutFragmentFrame
1007+
lineFragmentFrame.size.height = textLineFragment.typographicBounds.height
1008+
1009+
lineSelectionRectangle = CGRect(
1010+
origin: CGPoint(
1011+
x: selectionView.bounds.minX,
1012+
y: lineFragmentFrame.origin.y + textLineFragment.typographicBounds.minY
1013+
),
1014+
size: CGSize(
1015+
width: selectionView.bounds.width,
1016+
height: lineFragmentFrame.height
1017+
)
10131018
)
1014-
)
1015-
1019+
} else {
1020+
// Workaround for FB15131180
1021+
let prevTextLineFragment = layoutFragment.textLineFragments[layoutFragment.textLineFragments.count - 2]
1022+
var lineFragmentFrame = layoutFragment.layoutFragmentFrame
1023+
lineFragmentFrame.size.height = prevTextLineFragment.typographicBounds.height
1024+
1025+
lineSelectionRectangle = CGRect(
1026+
origin: CGPoint(
1027+
x: selectionView.bounds.minX,
1028+
y: lineFragmentFrame.origin.y + prevTextLineFragment.typographicBounds.maxY
1029+
),
1030+
size: CGSize(
1031+
width: selectionView.bounds.width,
1032+
height: lineFragmentFrame.height
1033+
)
1034+
)
1035+
}
1036+
10161037
if let rect = combinedFragmentsRect {
1017-
combinedFragmentsRect = rect.union(r)
1038+
combinedFragmentsRect = rect.union(lineSelectionRectangle)
10181039
} else {
1019-
combinedFragmentsRect = r
1040+
combinedFragmentsRect = lineSelectionRectangle
10201041
}
10211042
}
10221043
}

0 commit comments

Comments
 (0)