Skip to content

Commit 0f2d3a1

Browse files
committed
Add roundedPathForRange method to create a smooth bezier path for a text range
1 parent dfeb6eb commit 0f2d3a1

File tree

1 file changed

+88
-0
lines changed

1 file changed

+88
-0
lines changed

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Public.swift

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,94 @@ extension TextLayoutManager {
153153
)
154154
}
155155

156+
// swiftlint:disable function_body_length
157+
/// Creates a smooth bezier path for the specified range.
158+
/// If the range exceeds the available text, it uses the maximum available range.
159+
/// - Parameter range: The range of text offsets to generate the path for.
160+
/// - Returns: An `NSBezierPath` representing the visual shape for the text range, or `nil` if the range is invalid.
161+
public func roundedPathForRange(_ range: NSRange) -> NSBezierPath? {
162+
// Ensure the range is within the bounds of the text storage
163+
let validRange = NSRange(
164+
location: range.lowerBound,
165+
length: min(range.length, lineStorage.length - range.lowerBound)
166+
)
167+
168+
guard validRange.length > 0 else { return rectForEndOffset().map { NSBezierPath(rect: $0) } }
169+
170+
var rightSidePoints: [CGPoint] = [] // Points for Bottom-right → Top-right
171+
var leftSidePoints: [CGPoint] = [] // Points for Bottom-left → Top-left
172+
173+
var currentOffset = validRange.lowerBound
174+
175+
// Process each line fragment within the range
176+
while currentOffset < validRange.upperBound {
177+
guard let linePosition = lineStorage.getLine(atOffset: currentOffset) else { return nil }
178+
179+
if linePosition.data.lineFragments.isEmpty {
180+
let newHeight = ensureLayoutFor(position: linePosition)
181+
if linePosition.height != newHeight {
182+
delegate?.layoutManagerHeightDidUpdate(newHeight: lineStorage.height)
183+
}
184+
}
185+
186+
guard let fragmentPosition = linePosition.data.typesetter.lineFragments.getLine(
187+
atOffset: currentOffset - linePosition.range.location
188+
) else { break }
189+
190+
// Calculate the X positions for the range's boundaries within the fragment
191+
let realRangeStart = (textStorage?.string as? NSString)?
192+
.rangeOfComposedCharacterSequence(at: validRange.lowerBound)
193+
?? NSRange(location: validRange.lowerBound, length: 0)
194+
195+
let realRangeEnd = (textStorage?.string as? NSString)?
196+
.rangeOfComposedCharacterSequence(at: validRange.upperBound - 1)
197+
?? NSRange(location: validRange.upperBound - 1, length: 0)
198+
199+
let minXPos = CTLineGetOffsetForStringIndex(
200+
fragmentPosition.data.ctLine,
201+
realRangeStart.location - linePosition.range.location,
202+
nil
203+
) + edgeInsets.left
204+
205+
let maxXPos = CTLineGetOffsetForStringIndex(
206+
fragmentPosition.data.ctLine,
207+
realRangeEnd.upperBound - linePosition.range.location,
208+
nil
209+
) + edgeInsets.left
210+
211+
// Ensure the fragment has a valid width
212+
guard maxXPos > minXPos else { break }
213+
214+
// Add the Y positions for the fragment
215+
let topY = linePosition.yPos + fragmentPosition.yPos + fragmentPosition.data.scaledHeight
216+
let bottomY = linePosition.yPos + fragmentPosition.yPos
217+
218+
// Append points in the correct order
219+
rightSidePoints.append(contentsOf: [
220+
CGPoint(x: maxXPos, y: bottomY), // Bottom-right
221+
CGPoint(x: maxXPos, y: topY) // Top-right
222+
])
223+
leftSidePoints.insert(contentsOf: [
224+
CGPoint(x: minXPos, y: topY), // Top-left
225+
CGPoint(x: minXPos, y: bottomY) // Bottom-left
226+
], at: 0)
227+
228+
// Move to the next fragment
229+
currentOffset = min(validRange.upperBound, linePosition.range.upperBound)
230+
}
231+
232+
// Combine the points in clockwise order
233+
let points = leftSidePoints + rightSidePoints
234+
235+
// Close the path
236+
if let firstPoint = points.first {
237+
return NSBezierPath.smoothPath(points + [firstPoint], radius: 2)
238+
}
239+
240+
return nil
241+
}
242+
// swiftlint:enable function_body_length
243+
156244
/// Finds a suitable cursor rect for the end position.
157245
/// - Returns: A CGRect if it could be created.
158246
private func rectForEndOffset() -> CGRect? {

0 commit comments

Comments
 (0)