Skip to content

Commit

Permalink
Workaround FB15131180 - extra line fragment wrong frame
Browse files Browse the repository at this point in the history
  • Loading branch information
krzyzanowskim committed Sep 29, 2024
1 parent a7b54a3 commit 878565e
Show file tree
Hide file tree
Showing 6 changed files with 357 additions and 93 deletions.
78 changes: 64 additions & 14 deletions Sources/STTextViewAppKit/STTextView+Gutter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,11 @@ extension STTextView {
textLayoutManager.enumerateTextLayoutFragments(in: viewportRange, options: .ensuresLayout) { layoutFragment in
let contentRangeInElement = (layoutFragment.textElement as? NSTextParagraph)?.paragraphContentRange ?? layoutFragment.rangeInElement

for lineFragment in layoutFragment.textLineFragments where (lineFragment.isExtraLineFragment || layoutFragment.textLineFragments.first == lineFragment) {

for textLineFragment in layoutFragment.textLineFragments where (textLineFragment.isExtraLineFragment || layoutFragment.textLineFragments.first == textLineFragment) {
func isLineSelected() -> Bool {
textLayoutManager.textSelections.flatMap(\.textRanges).reduce(true) { partialResult, selectionTextRange in
var result = true
if lineFragment.isExtraLineFragment {
if textLineFragment.isExtraLineFragment {
let c1 = layoutFragment.rangeInElement.endLocation == selectionTextRange.location
result = result && c1
} else {
Expand All @@ -145,22 +144,73 @@ extension STTextView {
}

let isLineSelected = isLineSelected()
let lineNumber = startLineIndex + linesCount + 1

// calculated values depends on the "isExtraLineFragment" condition
var baselineYOffset: CGFloat = 0
if let paragraphStyle = lineFragment.attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle, !paragraphStyle.lineHeightMultiple.isAlmostZero() {
baselineYOffset = -(lineFragment.typographicBounds.height * (paragraphStyle.lineHeightMultiple - 1.0) / 2)
}
let locationForFirstCharacter: CGPoint
let lineFragmentFrame: CGRect

// The logic for extra line handling would use some cleanup
// It apply workaround for FB15131180 invalid frame being reported
// for the extra line fragment. The workaround is to calculate (adjust)
// extra line fragment frame based on previous text line (from the same layout fragment)
if layoutFragment.isExtraLineFragment {
if !textLineFragment.isExtraLineFragment {
locationForFirstCharacter = textLineFragment.locationForCharacter(at: 0)

if let paragraphStyle = textLineFragment.attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle, !paragraphStyle.lineHeightMultiple.isAlmostZero() {
baselineYOffset = -(textLineFragment.typographicBounds.height * (paragraphStyle.lineHeightMultiple - 1.0) / 2)
}

let lineNumber = startLineIndex + linesCount + 1
let locationForFirstCharacter = lineFragment.locationForCharacter(at: 0)
lineFragmentFrame = CGRect(
origin: CGPoint(
x: layoutFragment.layoutFragmentFrame.origin.x + textLineFragment.typographicBounds.origin.x,
y: layoutFragment.layoutFragmentFrame.origin.y + textLineFragment.typographicBounds.origin.y - scrollView.contentView.bounds.minY/*contentOffset.y*/
),
size: CGSize(
width: textLineFragment.typographicBounds.width,
height: textLineFragment.typographicBounds.height
)
)
} else {
// Use values from the same layoutFragment but previous line, that is not extra line fragment.
// Since this is extra line fragment, it is guaranteed that there is at least 2 line fragments in the layout fragment
let prevTextLineFragment = layoutFragment.textLineFragments[layoutFragment.textLineFragments.count - 2]
locationForFirstCharacter = prevTextLineFragment.locationForCharacter(at: 0)

if let paragraphStyle = prevTextLineFragment.attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle, !paragraphStyle.lineHeightMultiple.isAlmostZero() {
baselineYOffset = -(prevTextLineFragment.typographicBounds.height * (paragraphStyle.lineHeightMultiple - 1.0) / 2)
}

var lineFragmentFrame = CGRect(origin: CGPoint(x: 0, y: layoutFragment.layoutFragmentFrame.origin.y - scrollView.contentView.bounds.minY/*contentOffset.y*/), size: layoutFragment.layoutFragmentFrame.size)
lineFragmentFrame = CGRect(
origin: CGPoint(
x: layoutFragment.layoutFragmentFrame.origin.x + prevTextLineFragment.typographicBounds.origin.x,
y: layoutFragment.layoutFragmentFrame.origin.y + prevTextLineFragment.typographicBounds.maxY - scrollView.contentView.bounds.minY/*contentOffset.y*/
),
size: CGSize(
width: textLineFragment.typographicBounds.width,
height: prevTextLineFragment.typographicBounds.height
)
)
}
} else {
locationForFirstCharacter = textLineFragment.locationForCharacter(at: 0)

lineFragmentFrame.origin.y += lineFragment.typographicBounds.origin.y
if lineFragment.isExtraLineFragment {
lineFragmentFrame.size.height = lineFragment.typographicBounds.height
} else if !lineFragment.isExtraLineFragment, let extraLineFragment = layoutFragment.textLineFragments.first(where: { $0.isExtraLineFragment }) {
lineFragmentFrame.size.height -= extraLineFragment.typographicBounds.height
if let paragraphStyle = textLineFragment.attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle, !paragraphStyle.lineHeightMultiple.isAlmostZero() {
baselineYOffset = -(textLineFragment.typographicBounds.height * (paragraphStyle.lineHeightMultiple - 1.0) / 2)
}

lineFragmentFrame = CGRect(
origin: CGPoint(
x: layoutFragment.layoutFragmentFrame.origin.x + textLineFragment.typographicBounds.origin.x,
y: layoutFragment.layoutFragmentFrame.origin.y + textLineFragment.typographicBounds.origin.y - scrollView.contentView.bounds.minY/*contentOffset.y*/
),
size: CGSize(
width: layoutFragment.layoutFragmentFrame.width, // extend width to he fragment layout for the convenience of gutter
height: layoutFragment.layoutFragmentFrame.height
)
)
}

var effectiveLineTextAttributes = lineTextAttributes
Expand Down
88 changes: 72 additions & 16 deletions Sources/STTextViewAppKit/STTextView+InsertionPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import Foundation
import AppKit
import STTextKitPlus

extension STTextView {

Expand All @@ -15,28 +16,83 @@ extension STTextView {
return
}

let textSelectionFrames = insertionPointsRanges.compactMap { textRange -> CGRect? in

guard let textSegmentFrame = textLayoutManager.textSegmentFrame(in: textRange, type: .selection, options: .rangeNotRequired) else {
return nil
}

let selectionFrame = textSegmentFrame.intersection(frame)

// because `textLayoutManager.enumerateTextLayoutFragments(from: nil, options: [.ensuresExtraLineFragment, .ensuresLayout, .estimatesSize])`
// returns unexpected value for extra line fragment height (return 14) that is not correct in the context,
// therefore for empty override height with value manually calculated from font + paragraph style
if textRange == textLayoutManager.documentRange, textRange.isEmpty {
return CGRect(origin: selectionFrame.origin, size: CGSize(width: selectionFrame.width, height: typingLineHeight)).pixelAligned
// rewrite it to lines
var textSelectionFrames: [CGRect] = []
for selectionTextRange in insertionPointsRanges {
textLayoutManager.enumerateTextSegments(in: selectionTextRange, type: .standard) { textSegmentRange, textSegmentFrame, baselinePosition, textContainer in
if let textSegmentRange {
let documentRange = textLayoutManager.documentRange
guard !documentRange.isEmpty else {
// empty document
textSelectionFrames.append(
CGRect(
origin: CGPoint(
x: textSegmentFrame.origin.x,
y: textSegmentFrame.origin.y
),
size: CGSize(
width: textSegmentFrame.width,
height: typingLineHeight
)
)
)
return false
}

let isAtEndLocation = textSegmentRange.location == documentRange.endLocation
guard !isAtEndLocation else {
// At the end of non-empty document

// FB15131180: extra line fragment frame is not correct hence workaround location and height at extra line
if let layoutFragment = textLayoutManager.extraLineTextLayoutFragment() {
// at least 2 lines guaranteed at this point
let prevTextLineFragment = layoutFragment.textLineFragments[layoutFragment.textLineFragments.count - 2]
textSelectionFrames.append(
CGRect(
origin: CGPoint(
x: textSegmentFrame.origin.x,
y: layoutFragment.layoutFragmentFrame.origin.y + prevTextLineFragment.typographicBounds.maxY
),
size: CGSize(
width: textSegmentFrame.width,
height: prevTextLineFragment.typographicBounds.height
)
)
)
} else if let prevLocation = textLayoutManager.location(textSegmentRange.endLocation, offsetBy: -1),
let prevTextLineFragment = textLayoutManager.textLineFragment(at: prevLocation)
{
// Get insertion point height from the last-to-end (last) line fragment location
// since we're at the end location at this point.
textSelectionFrames.append(
CGRect(
origin: CGPoint(
x: textSegmentFrame.origin.x,
y: textSegmentFrame.origin.y
),
size: CGSize(
width: textSegmentFrame.width,
height: prevTextLineFragment.typographicBounds.height
)
)
)
}
return false
}

// Regular where segment frame is correct
textSelectionFrames.append(
textSegmentFrame
)
}
return true
}

return selectionFrame
}

removeInsertionPointView()

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

var textInsertionIndicator: any STInsertionPointIndicatorProtocol
if let customTextInsertionIndicator = self.delegateProxy.textViewInsertionPointView(self, frame: CGRect(origin: .zero, size: insertionViewFrame.size)) {
Expand Down
63 changes: 42 additions & 21 deletions Sources/STTextViewAppKit/STTextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -977,12 +977,12 @@ import AVFoundation
// extra line fragment area (sic).
textLayoutManager.enumerateTextLayoutFragments(in: viewportRange) { layoutFragment in
let contentRangeInElement = (layoutFragment.textElement as? NSTextParagraph)?.paragraphContentRange ?? layoutFragment.rangeInElement
for lineFragment in layoutFragment.textLineFragments {
for textLineFragment in layoutFragment.textLineFragments {

func isLineSelected() -> Bool {
textLayoutManager.textSelections.flatMap(\.textRanges).reduce(true) { partialResult, selectionTextRange in
var result = true
if lineFragment.isExtraLineFragment {
if textLineFragment.isExtraLineFragment {
let c1 = layoutFragment.rangeInElement.endLocation == selectionTextRange.location
result = result && c1
} else {
Expand All @@ -996,27 +996,48 @@ import AVFoundation
return partialResult && result
}
}

if isLineSelected() {
var lineFragmentFrame = layoutFragment.layoutFragmentFrame
lineFragmentFrame.size.height = lineFragment.typographicBounds.height


let r = CGRect(
origin: CGPoint(
x: selectionView.bounds.minX,
y: lineFragmentFrame.origin.y + lineFragment.typographicBounds.minY
),
size: CGSize(
width: selectionView.bounds.width,
height: lineFragmentFrame.height

let isLineSelected = isLineSelected()

if isLineSelected {
let lineSelectionRectangle: CGRect

if !textLineFragment.isExtraLineFragment {
var lineFragmentFrame = layoutFragment.layoutFragmentFrame
lineFragmentFrame.size.height = textLineFragment.typographicBounds.height

lineSelectionRectangle = CGRect(
origin: CGPoint(
x: selectionView.bounds.minX,
y: lineFragmentFrame.origin.y + textLineFragment.typographicBounds.minY
),
size: CGSize(
width: selectionView.bounds.width,
height: lineFragmentFrame.height
)
)
)

} else {
// Workaround for FB15131180
let prevTextLineFragment = layoutFragment.textLineFragments[layoutFragment.textLineFragments.count - 2]
var lineFragmentFrame = layoutFragment.layoutFragmentFrame
lineFragmentFrame.size.height = prevTextLineFragment.typographicBounds.height

lineSelectionRectangle = CGRect(
origin: CGPoint(
x: selectionView.bounds.minX,
y: lineFragmentFrame.origin.y + prevTextLineFragment.typographicBounds.maxY
),
size: CGSize(
width: selectionView.bounds.width,
height: lineFragmentFrame.height
)
)
}

if let rect = combinedFragmentsRect {
combinedFragmentsRect = rect.union(r)
combinedFragmentsRect = rect.union(lineSelectionRectangle)
} else {
combinedFragmentsRect = r
combinedFragmentsRect = lineSelectionRectangle
}
}
}
Expand Down
Loading

0 comments on commit 878565e

Please sign in to comment.