import Foundation #if os(macOS) import AppKit #else import UIKit #endif /// Personal layout only: never adds characters or attributes to CRDT content. /// Letter paper uses the exporter's 612 × 792 point geometry and 72 point margins. /// Narrow screens reflow text with smaller side margins, like the web editor. struct DocsPageGeometry: Equatable, Sendable { var viewportWidth: CGFloat var paged: Bool static let paperHeight: CGFloat = 792 static let verticalMargin: CGFloat = 72 static let gap: CGFloat = 24 var paperWidth: CGFloat { #if os(macOS) 612 #else min(612, max(160, viewportWidth - 24)) #endif } var paperX: CGFloat { max(0, (viewportWidth - paperWidth) / 2) } var sideMargin: CGFloat { paperWidth < 480 ? 20 : 72 } var textX: CGFloat { paperX + sideMargin } var textWidth: CGFloat { paperWidth - sideMargin * 2 } var stride: CGFloat { Self.paperHeight + Self.gap } var bodyHeight: CGFloat { Self.paperHeight - Self.verticalMargin * 2 } func page(at y: CGFloat) -> Int { paged ? max(0, Int(floor(max(0, y) / stride))) : 0 } func pageRect(_ index: Int) -> CGRect { CGRect(x: paperX, y: CGFloat(index) * stride, width: paperWidth, height: Self.paperHeight) } func lineRect(_ proposal: CGRect) -> CGRect { var rect = proposal rect.origin.x = textX + proposal.minX rect.size.width = max(0, min(proposal.width, textWidth - proposal.minX)) if !paged { rect.origin.y = max(Self.verticalMargin, proposal.minY) return rect } let index = page(at: proposal.minY) let top = CGFloat(index) * stride + Self.verticalMargin let bottom = top + bodyHeight rect.origin.y = max(top, proposal.minY) // Oversized attachment lines stay on one page instead of causing a loop. if rect.height <= bodyHeight && rect.maxY > bottom { rect.origin.y = CGFloat(index + 1) * stride + Self.verticalMargin } return rect } func documentHeight(usedHeight: CGFloat) -> CGFloat { if !paged { return max(Self.paperHeight, usedHeight + Self.verticalMargin) } return CGFloat(page(at: max(Self.verticalMargin, usedHeight - 1)) + 1) * stride - Self.gap } } final class DocsPageTextContainer: NSTextContainer { #if os(macOS) var blockStarts: [Int: CGFloat] = [:] { didSet { if oldValue != blockStarts { layoutManager?.textContainerChangedGeometry(self) } } } #endif var geometry = DocsPageGeometry(viewportWidth: 612, paged: false) { didSet { if oldValue != geometry { layoutManager?.textContainerChangedGeometry(self) } } } override var isSimpleRectangularTextContainer: Bool { false } override func lineFragmentRect( forProposedRect proposedRect: CGRect, at characterIndex: Int, writingDirection baseWritingDirection: NSWritingDirection, remaining remainingRect: UnsafeMutablePointer? ) -> CGRect { remainingRect?.pointee = .zero var rect = geometry.lineRect(proposedRect) #if os(macOS) if let top = blockStarts[characterIndex] { rect.origin.y = max(rect.minY, top) } #endif if let storage = layoutManager?.textStorage, characterIndex < storage.length { if characterIndex == 0, let margin = storage.attribute(.docsFirstMargin, at: 0, effectiveRange: nil) as? CGFloat { rect.origin.y = max(rect.minY, DocsPageGeometry.verticalMargin + margin) } var range = NSRange() if let padding = storage.attribute(.docsCodeStart, at: characterIndex, effectiveRange: &range) as? CGFloat, range.location == characterIndex { rect.origin.x += padding rect.size.width = max(0, rect.width - padding) } } return rect } }