#if os(macOS) import AppKit extension NSAttributedString.Key { static let docsBaseFont = NSAttributedString.Key("OficinaDocsBaseFont") static let docsLeading = NSAttributedString.Key("OficinaDocsLeading") } /// CSS line boxes include the paragraph's font strut and each inline run's /// half-leading. TextKit's single paragraph multiplier cannot express that when /// a line mixes font families or sizes. @MainActor final class DocsLayoutManager: NSLayoutManager, @preconcurrency NSLayoutManagerDelegate { private(set) var storageRevision: UInt64 = 0 override func processEditing(for textStorage: NSTextStorage, edited editMask: NSTextStorageEditActions, range newCharRange: NSRange, changeInLength delta: Int, invalidatedRange invalidatedCharRange: NSRange) { super.processEditing(for: textStorage, edited: editMask, range: newCharRange, changeInLength: delta, invalidatedRange: invalidatedCharRange) storageRevision &+= 1 for container in textContainers { (container.textView as? DocsPagedTextView)?.invalidatePagination() } } override init() { super.init(); usesFontLeading = false; delegate = self } required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") } override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: NSPoint) { super.drawBackground(forGlyphRange: glyphsToShow, at: origin) guard let storage = textStorage, let container = textContainers.first as? DocsPageTextContainer else { return } let characters = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil) storage.enumerateAttribute(.docsQuoteBorders, in: characters) { value, range, _ in guard let borders = value as? [CGFloat] else { return } let glyphs = self.glyphRange(forCharacterRange: range, actualCharacterRange: nil) var bounds = self.boundingRect(forGlyphRange: glyphs, in: container) bounds.origin.y += origin.y NSColor(white: 200.0 / 255, alpha: 1).setFill() for inset in borders { CGRect(x: origin.x + container.geometry.textX + inset, y: bounds.minY, width: 2.25, height: bounds.height).fill() } } } func layoutManager(_ layoutManager: NSLayoutManager, shouldSetLineFragmentRect lineFragmentRect: UnsafeMutablePointer, lineFragmentUsedRect: UnsafeMutablePointer, baselineOffset: UnsafeMutablePointer, in textContainer: NSTextContainer, forGlyphRange glyphRange: NSRange) -> Bool { guard let storage = textStorage, glyphRange.length > 0 else { return false } let range = characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) guard range.location < storage.length, let base = storage.attribute(.docsBaseFont, at: range.location, effectiveRange: nil) as? NSFont, let leading = storage.attribute(.docsLeading, at: range.location, effectiveRange: nil) as? CGFloat else { return false } var above: CGFloat = 0, below: CGFloat = 0 func include(_ font: NSFont) { let halfLeading = (font.pointSize * leading - font.ascender + font.descender) / 2 above = max(above, font.ascender + halfLeading) below = max(below, -font.descender + halfLeading) } include(base) var attachment = false storage.enumerateAttributes(in: range) { attributes, _, _ in if let font = attributes[.font] as? NSFont { include(font) } if attributes[.attachment] != nil { attachment = true } } guard !attachment else { return false } let end = NSMaxRange(range) let endsParagraph = end > 0 && (storage.string as NSString).character(at: end - 1) == 10 let style = storage.attribute(.paragraphStyle, at: range.location, effectiveRange: nil) as? NSParagraphStyle lineFragmentRect.pointee.size.height = above + below + (endsParagraph ? style?.paragraphSpacing ?? 0 : 0) lineFragmentUsedRect.pointee.size.height = above + below baselineOffset.pointee = above return true } } #endif