oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2#if os(macOS)
3import AppKit
4
5@MainActor class DocsPagedTextView: NSTextView {
6 var paged = false { didSet { if oldValue != paged { needsLayout = true; needsDisplay = true } } }
7 private var updatingGeometry = false
8 private var updatingPagination = false
9 private var separators: [CGFloat] = []
10 private var measuredRevision: UInt64?
11 private var measuredWidth: CGFloat = 0
12 private var measuredPaged = false
13 private var measuredContent: NSAttributedString?
14 func invalidatePagination() {
15 measuredRevision = nil
16 // TextKit may ask for caret geometry before the next NSView layout.
17 // Prepare the complete page plan now, on an independent layout stack.
18 if !updatingGeometry { updatePagination() }
19 needsLayout = true
20 }
21 override init(frame frameRect: NSRect, textContainer container: NSTextContainer?) {
22 let storage = NSTextStorage()
23 let manager = DocsLayoutManager()
24 manager.usesFontLeading = false
25 let pages = DocsPageTextContainer(size: CGSize(width: max(320, frameRect.width), height: 1_000_000))
26 pages.lineFragmentPadding = 0
27 storage.addLayoutManager(manager)
28 manager.addTextContainer(pages)
29 super.init(frame: frameRect, textContainer: pages)
30 textContainerInset = .zero
31 // This view owns its paginated height; NSTextView auto-sizing uses a
32 // different used rect and briefly clips the current line during input.
33 isVerticallyResizable = false
34 isHorizontallyResizable = false
35 drawsBackground = true
36 textColor = .black
37 }
38 override convenience init(frame: NSRect) { self.init(frame: frame, textContainer: nil) }
39 required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
40 // Page margins are already part of DocsPageTextContainer's line geometry.
41 // NSTextView's inferred origin moves during incremental layout and can put
42 // its native insertion indicator at a different origin from the glyphs.
43 override var textContainerOrigin: NSPoint { .zero }
44 override func didChangeText() {
45 // Size the document before AppKit scrolls the newly edited caret.
46 needsLayout = true
47 layoutSubtreeIfNeeded()
48 super.didChangeText()
49 }
50 private var viewportWidth: CGFloat { max(636, enclosingScrollView?.contentView.bounds.width ?? bounds.width) }
51 private func updatePagination() {
52 guard !updatingPagination else { return }
53 updatingPagination = true
54 defer { updatingPagination = false }
55 guard let pages = textContainer as? DocsPageTextContainer,
56 let manager = layoutManager as? DocsLayoutManager, let storage = textStorage else { return }
57 let width = viewportWidth
58 let size = CGSize(width: width, height: 1_000_000)
59 if pages.size != size { pages.size = size }
60 pages.geometry = DocsPageGeometry(viewportWidth: width, paged: false)
61 let revision = manager.storageRevision
62 guard measuredRevision != revision || measuredWidth != width || measuredPaged != paged else { return }
63 let content = paged ? DocumentPageLayout.measurementContent(storage) : nil
64 if measuredWidth == width, measuredPaged == paged, let content, measuredContent?.isEqual(to: content) == true {
65 measuredRevision = revision
66 return
67 }
68 var plan = DocumentPageLayout.Result()
69 if let content {
70 // Never clear the live container to measure unpaginated content: doing
71 // so exposes a second set of caret/scroll coordinates during edits.
72 let measurement = NSTextStorage(attributedString: content)
73 let measurementManager = DocsLayoutManager()
74 let measurementContainer = DocsPageTextContainer(size: size)
75 measurementContainer.lineFragmentPadding = 0
76 measurementContainer.geometry = pages.geometry
77 measurement.addLayoutManager(measurementManager)
78 measurementManager.addTextContainer(measurementContainer)
79 measurementManager.ensureLayout(for: measurementContainer)
80 plan = DocumentPageLayout.measure(storage: measurement, manager: measurementManager, container: measurementContainer)
81 }
82 if pages.blockStarts != plan.starts || separators != plan.separators { needsDisplay = true }
83 pages.blockStarts = plan.starts
84 separators = plan.separators
85 measuredRevision = revision; measuredWidth = width; measuredPaged = paged
86 measuredContent = content
87 }
88 override func layout() {
89 guard !updatingGeometry, let pages = textContainer as? DocsPageTextContainer,
90 let manager = layoutManager else { return }
91 updatingGeometry = true
92 defer { updatingGeometry = false }
93 updatePagination()
94 // AppKit must only lay out the caret after final page coordinates exist.
95 super.layout()
96 manager.ensureLayout(for: pages)
97 let width = viewportWidth
98 let bottom = manager.numberOfGlyphs > 0
99 ? manager.lineFragmentRect(forGlyphAt: manager.numberOfGlyphs - 1, effectiveRange: nil).maxY : DocsPageGeometry.verticalMargin
100 let height = pages.geometry.documentHeight(usedHeight: bottom)
101 if abs(frame.width - width) > 0.5 || abs(frame.height - height) > 0.5 {
102 setFrameSize(CGSize(width: width, height: height))
103 }
104 }
105 override func drawBackground(in dirtyRect: NSRect) {
106 NSGraphicsContext.saveGraphicsState()
107 defer { NSGraphicsContext.restoreGraphicsState() }
108 dirtyRect.clip()
109 NSColor.windowBackgroundColor.setFill()
110 dirtyRect.fill()
111 if let pages = textContainer as? DocsPageTextContainer {
112 let geometry = pages.geometry
113 NSColor.white.setFill()
114 CGRect(x: geometry.paperX, y: 0, width: geometry.paperWidth, height: bounds.height).fill()
115 if paged {
116 for (index, y) in separators.enumerated() where y + 18 >= dirtyRect.minY && y <= dirtyRect.maxY {
117 NSColor.windowBackgroundColor.setFill()
118 CGRect(x: geometry.paperX, y: y, width: geometry.paperWidth, height: 18).fill()
119 let label = String(localized: "docs.editor.page.number", bundle: DocsResources.bundle)
120 .replacingOccurrences(of: "{n}", with: String(index + 2))
121 (label as NSString).draw(at: CGPoint(x: geometry.paperX + geometry.paperWidth - 80, y: y + 3),
122 withAttributes: [.font: NSFont.systemFont(ofSize: 10), .foregroundColor: NSColor.darkGray])
123 }
124 }
125 }
126 }
127}
128#else
129import UIKit
130
131@MainActor class DocsPagedTextView: UITextView {
132 var paged = false { didSet { if oldValue != paged { setNeedsLayout(); setNeedsDisplay() } } }
133 private var updatingGeometry = false
134 override init(frame: CGRect = .zero, textContainer: NSTextContainer? = nil) {
135 let storage = NSTextStorage()
136 let manager = NSLayoutManager()
137 manager.usesFontLeading = false
138 let pages = DocsPageTextContainer(size: CGSize(width: max(320, frame.width), height: 1_000_000))
139 pages.lineFragmentPadding = 0
140 storage.addLayoutManager(manager)
141 manager.addTextContainer(pages)
142 super.init(frame: frame, textContainer: pages)
143 textContainerInset = .zero
144 backgroundColor = .clear
145 textColor = .black
146 isOpaque = false
147 }
148 required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
149 override func layoutSubviews() {
150 super.layoutSubviews()
151 guard !updatingGeometry, let pages = textContainer as? DocsPageTextContainer else { return }
152 updatingGeometry = true
153 defer { updatingGeometry = false }
154 let width = max(160, bounds.width)
155 let size = CGSize(width: width, height: 1_000_000)
156 if pages.size != size { pages.size = size }
157 pages.geometry = DocsPageGeometry(viewportWidth: width, paged: paged)
158 layoutManager.ensureLayout(for: pages)
159 let height = pages.geometry.documentHeight(usedHeight: layoutManager.usedRect(for: pages).maxY)
160 if abs(contentSize.height - height) > 0.5 || abs(contentSize.width - width) > 0.5 {
161 contentSize = CGSize(width: width, height: height)
162 }
163 setNeedsDisplay()
164 }
165 override func draw(_ rect: CGRect) {
166 UIColor.systemGroupedBackground.setFill()
167 UIRectFill(rect)
168 if let pages = textContainer as? DocsPageTextContainer {
169 let geometry = pages.geometry
170 if paged {
171 for page in geometry.page(at: rect.minY)...geometry.page(at: rect.maxY) {
172 UIColor.white.setFill()
173 UIRectFill(geometry.pageRect(page))
174 let label = String(localized: "docs.editor.page.number", bundle: DocsResources.bundle)
175 .replacingOccurrences(of: "{n}", with: String(page + 1))
176 (label as NSString).draw(at: CGPoint(x: geometry.paperX + geometry.paperWidth - 80,
177 y: CGFloat(page) * geometry.stride + DocsPageGeometry.paperHeight - 35),
178 withAttributes: [.font: UIFont.systemFont(ofSize: 10), .foregroundColor: UIColor.darkGray])
179 }
180 } else {
181 UIColor.white.setFill()
182 UIRectFill(CGRect(x: geometry.paperX, y: 0, width: geometry.paperWidth, height: max(bounds.height, contentSize.height)))
183 }
184 }
185 super.draw(rect)
186 }
187}
188#endif
189