@@ -30,8 +30,13 @@ protocol LayoutManagerDelegate: AnyObject {
30
30
var textContainerInset : UIEdgeInsets { get }
31
31
32
32
var listLineFormatting : LineFormatting { get }
33
+
34
+ var isLineNumbersEnabled : Bool { get }
35
+ var lineNumberFormatting : LineNumberFormatting { get }
36
+ var lineNumberWrappingMarker : String ? { get }
33
37
34
38
func listMarkerForItem( at index: Int , level: Int , previousLevel: Int , attributeValue: Any ? ) -> ListLineMarker
39
+ func lineNumberString( for index: Int ) -> String ?
35
40
}
36
41
37
42
class LayoutManager : NSLayoutManager {
@@ -270,11 +275,33 @@ class LayoutManager: NSLayoutManager {
270
275
return stringRect
271
276
}
272
277
278
+ private func rectForLineNumbers( markerSize: CGSize , rect: CGRect , width: CGFloat ) -> CGRect {
279
+ let topInset = layoutManagerDelegate? . textContainerInset. top ?? 0
280
+ let spacerRect = CGRect ( origin: CGPoint ( x: 0 , y: topInset) , size: CGSize ( width: width, height: rect. height) )
281
+
282
+ let scaleFactor = markerSize. height / spacerRect. height
283
+ var markerSizeToUse = markerSize
284
+ // Resize maintaining aspect ratio if bullet height is more than available line height
285
+ if scaleFactor > 1 {
286
+ markerSizeToUse = CGSize ( width: markerSize. width/ scaleFactor, height: markerSize. height/ scaleFactor)
287
+ }
288
+
289
+ let trailingPadding : CGFloat = 2
290
+ let yPos = topInset + rect. minY
291
+ let stringRect = CGRect ( origin: CGPoint ( x: spacerRect. maxX - markerSizeToUse. width - trailingPadding, y: yPos) , size: markerSizeToUse)
292
+
293
+ // debugRect(rect: spacerRect, color: .blue)
294
+ // debugRect(rect: stringRect, color: .red)
295
+
296
+ return stringRect
297
+ }
298
+
273
299
override func drawBackground( forGlyphRange glyphsToShow: NSRange , at origin: CGPoint ) {
274
300
super. drawBackground ( forGlyphRange: glyphsToShow, at: origin)
275
301
guard let textStorage = textStorage,
276
302
let currentCGContext = UIGraphicsGetCurrentContext ( )
277
303
else { return }
304
+ currentCGContext. saveGState ( )
278
305
279
306
let characterRange = self . characterRange ( forGlyphRange: glyphsToShow, actualGlyphRange: nil )
280
307
textStorage. enumerateAttribute ( . backgroundStyle, in: characterRange) { attr, bgStyleRange, _ in
@@ -321,6 +348,54 @@ class LayoutManager: NSLayoutManager {
321
348
drawBackground ( backgroundStyle: backgroundStyle, rects: rects, currentCGContext: currentCGContext)
322
349
}
323
350
}
351
+ drawLineNumbers ( textStorage: textStorage, currentCGContext: currentCGContext)
352
+ currentCGContext. restoreGState ( )
353
+ }
354
+
355
+ private func drawLineNumbers( textStorage: NSTextStorage , currentCGContext: CGContext ) {
356
+ var lineNumber = 1
357
+ guard layoutManagerDelegate? . isLineNumbersEnabled == true ,
358
+ let lineNumberFormatting = layoutManagerDelegate? . lineNumberFormatting else { return }
359
+
360
+ let lineNumberWrappingMarker = layoutManagerDelegate? . lineNumberWrappingMarker
361
+ enumerateLineFragments ( forGlyphRange: textStorage. fullRange) { [ weak self] rect, usedRect, _, range, _ in
362
+ guard let self else { return }
363
+ let paraRange = self . textStorage? . mutableString. paragraphRange ( for: range) . firstCharacterRange
364
+ let lineNumberToDisplay = layoutManagerDelegate? . lineNumberString ( for: lineNumber) ?? " \( lineNumber) "
365
+
366
+ if range. location == paraRange? . location {
367
+ self . drawLineNumber ( lineNumber: lineNumberToDisplay, rect: rect. integral, lineNumberFormatting: lineNumberFormatting, currentCGContext: currentCGContext)
368
+ lineNumber += 1
369
+ } else if let lineNumberWrappingMarker {
370
+ self . drawLineNumber ( lineNumber: lineNumberWrappingMarker, rect: rect. integral, lineNumberFormatting: lineNumberFormatting, currentCGContext: currentCGContext)
371
+ }
372
+ }
373
+
374
+ // Draw line number for additional new line with \n, if exists
375
+ drawLineNumber ( lineNumber: " \( lineNumber) " , rect: extraLineFragmentRect. integral, lineNumberFormatting: lineNumberFormatting, currentCGContext: currentCGContext)
376
+ }
377
+
378
+ func drawLineNumber( lineNumber: String , rect: CGRect , lineNumberFormatting: LineNumberFormatting , currentCGContext: CGContext ) {
379
+ let gutterWidth = lineNumberFormatting. gutter. width
380
+ let attributes = lineNumberAttributes ( lineNumberFormatting: lineNumberFormatting)
381
+ let text = NSAttributedString ( string: " \( lineNumber) " , attributes: attributes)
382
+ let markerSize = text. boundingRect ( with: . zero, options: [ ] , context: nil ) . integral. size
383
+ var markerRect = self . rectForLineNumbers ( markerSize: markerSize, rect: rect, width: gutterWidth)
384
+ let listMarkerImage = self . generateBitmap ( string: text, rect: markerRect)
385
+ listMarkerImage. draw ( at: markerRect. origin)
386
+ }
387
+
388
+ private func lineNumberAttributes( lineNumberFormatting: LineNumberFormatting ) -> [ NSAttributedString . Key : Any ] {
389
+ let font = lineNumberFormatting. font
390
+ let textColor = lineNumberFormatting. textColor
391
+ let paraStyle = NSMutableParagraphStyle ( )
392
+ paraStyle. alignment = . right
393
+
394
+ return [
395
+ NSAttributedString . Key. font: font,
396
+ NSAttributedString . Key. foregroundColor: textColor,
397
+ NSAttributedString . Key. paragraphStyle: paraStyle
398
+ ]
324
399
}
325
400
326
401
private func drawBackground( backgroundStyle: BackgroundStyle , rects: [ CGRect ] , currentCGContext: CGContext ) {
0 commit comments