| 1 | #if os(macOS) |
| 2 | import AppKit |
| 3 | |
| 4 | extension NSAttributedString.Key { |
| 5 | static let docsBaseFont = NSAttributedString.Key("OficinaDocsBaseFont") |
| 6 | static let docsLeading = NSAttributedString.Key("OficinaDocsLeading") |
| 7 | } |
| 8 | |
| 9 | /// CSS line boxes include the paragraph's font strut and each inline run's |
| 10 | /// half-leading. TextKit's single paragraph multiplier cannot express that when |
| 11 | /// a line mixes font families or sizes. |
| 12 | @MainActor final class DocsLayoutManager: NSLayoutManager, @preconcurrency NSLayoutManagerDelegate { |
| 13 | private(set) var storageRevision: UInt64 = 0 |
| 14 | override func processEditing(for textStorage: NSTextStorage, edited editMask: NSTextStorageEditActions, |
| 15 | range newCharRange: NSRange, changeInLength delta: Int, invalidatedRange invalidatedCharRange: NSRange) { |
| 16 | super.processEditing(for: textStorage, edited: editMask, range: newCharRange, |
| 17 | changeInLength: delta, invalidatedRange: invalidatedCharRange) |
| 18 | storageRevision &+= 1 |
| 19 | for container in textContainers { |
| 20 | (container.textView as? DocsPagedTextView)?.invalidatePagination() |
| 21 | } |
| 22 | } |
| 23 | override init() { super.init(); usesFontLeading = false; delegate = self } |
| 24 | required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") } |
| 25 | override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: NSPoint) { |
| 26 | super.drawBackground(forGlyphRange: glyphsToShow, at: origin) |
| 27 | guard let storage = textStorage, let container = textContainers.first as? DocsPageTextContainer else { return } |
| 28 | let characters = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil) |
| 29 | storage.enumerateAttribute(.docsQuoteBorders, in: characters) { value, range, _ in |
| 30 | guard let borders = value as? [CGFloat] else { return } |
| 31 | let glyphs = self.glyphRange(forCharacterRange: range, actualCharacterRange: nil) |
| 32 | var bounds = self.boundingRect(forGlyphRange: glyphs, in: container) |
| 33 | bounds.origin.y += origin.y |
| 34 | NSColor(white: 200.0 / 255, alpha: 1).setFill() |
| 35 | for inset in borders { |
| 36 | CGRect(x: origin.x + container.geometry.textX + inset, y: bounds.minY, width: 2.25, height: bounds.height).fill() |
| 37 | } |
| 38 | } |
| 39 | } |
| 40 | func layoutManager(_ layoutManager: NSLayoutManager, shouldSetLineFragmentRect lineFragmentRect: UnsafeMutablePointer<NSRect>, |
| 41 | lineFragmentUsedRect: UnsafeMutablePointer<NSRect>, baselineOffset: UnsafeMutablePointer<CGFloat>, |
| 42 | in textContainer: NSTextContainer, forGlyphRange glyphRange: NSRange) -> Bool { |
| 43 | guard let storage = textStorage, glyphRange.length > 0 else { return false } |
| 44 | let range = characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) |
| 45 | guard range.location < storage.length, |
| 46 | let base = storage.attribute(.docsBaseFont, at: range.location, effectiveRange: nil) as? NSFont, |
| 47 | let leading = storage.attribute(.docsLeading, at: range.location, effectiveRange: nil) as? CGFloat else { return false } |
| 48 | var above: CGFloat = 0, below: CGFloat = 0 |
| 49 | func include(_ font: NSFont) { |
| 50 | let halfLeading = (font.pointSize * leading - font.ascender + font.descender) / 2 |
| 51 | above = max(above, font.ascender + halfLeading) |
| 52 | below = max(below, -font.descender + halfLeading) |
| 53 | } |
| 54 | include(base) |
| 55 | var attachment = false |
| 56 | storage.enumerateAttributes(in: range) { attributes, _, _ in |
| 57 | if let font = attributes[.font] as? NSFont { include(font) } |
| 58 | if attributes[.attachment] != nil { attachment = true } |
| 59 | } |
| 60 | guard !attachment else { return false } |
| 61 | let end = NSMaxRange(range) |
| 62 | let endsParagraph = end > 0 && (storage.string as NSString).character(at: end - 1) == 10 |
| 63 | let style = storage.attribute(.paragraphStyle, at: range.location, effectiveRange: nil) as? NSParagraphStyle |
| 64 | lineFragmentRect.pointee.size.height = above + below + (endsParagraph ? style?.paragraphSpacing ?? 0 : 0) |
| 65 | lineFragmentUsedRect.pointee.size.height = above + below |
| 66 | baselineOffset.pointee = above |
| 67 | return true |
| 68 | } |
| 69 | } |
| 70 | #endif |
| 71 | |