oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1#if os(macOS)
2import AppKit
3
4extension 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