@@ -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