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#else
5import UIKit
6#endif
7
8/// Personal layout only: never adds characters or attributes to CRDT content.
9/// Letter paper uses the exporter's 612 × 792 point geometry and 72 point margins.
10/// Narrow screens reflow text with smaller side margins, like the web editor.
11struct DocsPageGeometry: Equatable, Sendable {
12 var viewportWidth: CGFloat
13 var paged: Bool
14 static let paperHeight: CGFloat = 792
15 static let verticalMargin: CGFloat = 72
16 static let gap: CGFloat = 24
17 var paperWidth: CGFloat {
18 #if os(macOS)
19 612
20 #else
21 min(612, max(160, viewportWidth - 24))
22 #endif
23 }
24 var paperX: CGFloat { max(0, (viewportWidth - paperWidth) / 2) }
25 var sideMargin: CGFloat { paperWidth < 480 ? 20 : 72 }
26 var textX: CGFloat { paperX + sideMargin }
27 var textWidth: CGFloat { paperWidth - sideMargin * 2 }
28 var stride: CGFloat { Self.paperHeight + Self.gap }
29 var bodyHeight: CGFloat { Self.paperHeight - Self.verticalMargin * 2 }
30 func page(at y: CGFloat) -> Int { paged ? max(0, Int(floor(max(0, y) / stride))) : 0 }
31 func pageRect(_ index: Int) -> CGRect {
32 CGRect(x: paperX, y: CGFloat(index) * stride, width: paperWidth, height: Self.paperHeight)
33 }
34 func lineRect(_ proposal: CGRect) -> CGRect {
35 var rect = proposal
36 rect.origin.x = textX + proposal.minX
37 rect.size.width = max(0, min(proposal.width, textWidth - proposal.minX))
38 if !paged {
39 rect.origin.y = max(Self.verticalMargin, proposal.minY)
40 return rect
41 }
42 let index = page(at: proposal.minY)
43 let top = CGFloat(index) * stride + Self.verticalMargin
44 let bottom = top + bodyHeight
45 rect.origin.y = max(top, proposal.minY)
46 // Oversized attachment lines stay on one page instead of causing a loop.
47 if rect.height <= bodyHeight && rect.maxY > bottom {
48 rect.origin.y = CGFloat(index + 1) * stride + Self.verticalMargin
49 }
50 return rect
51 }
52 func documentHeight(usedHeight: CGFloat) -> CGFloat {
53 if !paged { return max(Self.paperHeight, usedHeight + Self.verticalMargin) }
54 return CGFloat(page(at: max(Self.verticalMargin, usedHeight - 1)) + 1) * stride - Self.gap
55 }
56}
57
58final class DocsPageTextContainer: NSTextContainer {
59 #if os(macOS)
60 var blockStarts: [Int: CGFloat] = [:] {
61 didSet { if oldValue != blockStarts { layoutManager?.textContainerChangedGeometry(self) } }
62 }
63 #endif
64 var geometry = DocsPageGeometry(viewportWidth: 612, paged: false) {
65 didSet {
66 if oldValue != geometry { layoutManager?.textContainerChangedGeometry(self) }
67 }
68 }
69 override var isSimpleRectangularTextContainer: Bool { false }
70 override func lineFragmentRect(
71 forProposedRect proposedRect: CGRect, at characterIndex: Int,
72 writingDirection baseWritingDirection: NSWritingDirection,
73 remaining remainingRect: UnsafeMutablePointer<CGRect>?
74 ) -> CGRect {
75 remainingRect?.pointee = .zero
76 var rect = geometry.lineRect(proposedRect)
77 #if os(macOS)
78 if let top = blockStarts[characterIndex] { rect.origin.y = max(rect.minY, top) }
79 #endif
80 if let storage = layoutManager?.textStorage, characterIndex < storage.length {
81 if characterIndex == 0, let margin = storage.attribute(.docsFirstMargin, at: 0, effectiveRange: nil) as? CGFloat {
82 rect.origin.y = max(rect.minY, DocsPageGeometry.verticalMargin + margin)
83 }
84 var range = NSRange()
85 if let padding = storage.attribute(.docsCodeStart, at: characterIndex, effectiveRange: &range) as? CGFloat,
86 range.location == characterIndex {
87 rect.origin.x += padding
88 rect.size.width = max(0, rect.width - padding)
89 }
90 }
91 return rect
92 }
93}
94