Files
ICCery-CPU/Sources/Views/TargetCanvasView.swift
T
Local Admin 43bff18101
Build macOS Packages / Build macOS (Intel) (push) Failing after 10s
Build macOS Packages / Build macOS (Apple Silicon) (push) Failing after 10s
Build macOS Packages / Build macOS (Universal) (push) Skipped
removed cruft and restructured.
2026-09-07 05:21:44 -07:00

208 lines
8.0 KiB
Swift

import AppKit
/// SPEC §6.4 / §8 / §9 — document view used for both screen preview and printing.
/// Pagination via `knowsPageRange` / `rectForPage`. Each page is drawn in isolation.
final class TargetCanvasView: NSView {
var pages: [TargetPage] = [] {
didSet { needsDisplay = true }
}
var selectedIndex: Int = 0 {
didSet { needsDisplay = true }
}
var printInfo: NSPrintInfo = NSPrintInfo.shared
var centered: Bool = true {
didSet { needsDisplay = true }
}
var scaling: CGFloat = 1.0 {
didSet { needsDisplay = true }
}
var showImageable: Bool = false {
didSet { needsDisplay = true }
}
var airPrintWarning: String?
/// 1-based page index used by the print pipeline.
private var printingPage: Int = 1
override var isFlipped: Bool { true }
override var isOpaque: Bool { true }
override var acceptsFirstResponder: Bool { true }
var currentPage: TargetPage? {
guard !pages.isEmpty else { return nil }
let idx = min(max(selectedIndex, 0), pages.count - 1)
return pages[idx]
}
func currentPaper() -> Geometry.Paper {
let name = printInfo.paperName.map { $0.rawValue } ?? "A4"
var paper = Geometry.paper(named: name)
if printInfo.paperSize.width > 0 && printInfo.paperSize.height > 0 {
paper.widthPt = printInfo.paperSize.width
paper.heightPt = printInfo.paperSize.height
}
return paper
}
func imageableRect() -> CGRect {
let bounds = printInfo.imageablePageBounds
if bounds.width > 1 && bounds.height > 1 {
// `imageablePageBounds` is in the unflipped page coordinate system
// (origin bottom-left). Convert to our flipped view.
let paper = currentPaper()
return CGRect(
x: bounds.minX,
y: paper.heightPt - bounds.maxY,
width: bounds.width,
height: bounds.height
)
}
return Geometry.imageableBounds(paper: currentPaper())
}
func geometry(for page: TargetPage) -> Geometry.Report {
Geometry.report(
pixelWidth: CGFloat(page.pixelWidth),
pixelHeight: CGFloat(page.pixelHeight),
dpiX: page.dpiX,
dpiY: page.dpiY,
paper: currentPaper(),
imageable: imageableRect(),
centered: centered,
scaling: scaling
)
}
// MARK: - Pagination (SPEC §8)
override func knowsPageRange(_ range: NSRangePointer) -> Bool {
range.pointee = NSRange(location: 1, length: max(pages.count, 0))
return pages.count > 0
}
override func rectForPage(_ pageNumber: Int) -> NSRect {
printingPage = pageNumber
let size = printInfo.paperSize
return NSRect(origin: .zero, size: size)
}
override var pageHeader: NSAttributedString { NSAttributedString(string: "") }
override var pageFooter: NSAttributedString { NSAttributedString(string: "") }
// MARK: - Drawing
override func draw(_ dirtyRect: NSRect) {
guard let context = NSGraphicsContext.current?.cgContext else { return }
let printing = !(NSGraphicsContext.current?.isDrawingToScreen ?? true)
if printing {
drawPrintedPage(context: context)
} else {
drawPreview(context: context, dirty: dirtyRect)
}
}
private func drawPrintedPage(context: CGContext) {
// Reset so page N never inherits a transform from page N-1 (SPEC §8).
context.saveGState()
context.interpolationQuality = .none
context.setShouldAntialias(false)
context.setAllowsAntialiasing(false)
context.setShouldSmoothFonts(false)
let idx = printingPage - 1
guard idx >= 0, idx < pages.count else {
context.restoreGState()
return
}
let page = pages[idx]
let geo = geometry(for: page)
context.setFillColor(CGColor(gray: 1, alpha: 1))
context.fill(geo.paper)
context.draw(page.cgImage, in: geo.dest)
context.restoreGState()
}
private func drawPreview(context: CGContext, dirty: NSRect) {
context.setFillColor(NSColor(calibratedWhite: 0.07, alpha: 1).cgColor)
context.fill(bounds)
let paper = currentPaper()
let pad: CGFloat = 36
let fit = min(
max(1, (bounds.width - pad * 2) / max(paper.widthPt, 1)),
max(1, (bounds.height - pad * 2) / max(paper.heightPt, 1))
)
let paperCssW = paper.widthPt * fit
let paperCssH = paper.heightPt * fit
let ox = (bounds.width - paperCssW) / 2
let oy = (bounds.height - paperCssH) / 2
let paperRect = CGRect(x: ox, y: oy, width: paperCssW, height: paperCssH)
// Crop marks
context.setStrokeColor(NSColor(calibratedWhite: 0.83, alpha: 0.35).cgColor)
context.setLineWidth(1)
let mark: CGFloat = 10
context.beginPath()
context.move(to: CGPoint(x: ox - 12, y: oy)); context.addLine(to: CGPoint(x: ox - 12 + mark, y: oy))
context.move(to: CGPoint(x: ox, y: oy - 12)); context.addLine(to: CGPoint(x: ox, y: oy - 12 + mark))
context.move(to: CGPoint(x: ox + paperCssW + 12, y: oy)); context.addLine(to: CGPoint(x: ox + paperCssW + 12 - mark, y: oy))
context.move(to: CGPoint(x: ox + paperCssW, y: oy - 12)); context.addLine(to: CGPoint(x: ox + paperCssW, y: oy - 12 + mark))
context.move(to: CGPoint(x: ox - 12, y: oy + paperCssH)); context.addLine(to: CGPoint(x: ox - 12 + mark, y: oy + paperCssH))
context.move(to: CGPoint(x: ox, y: oy + paperCssH + 12)); context.addLine(to: CGPoint(x: ox, y: oy + paperCssH + 12 - mark))
context.move(to: CGPoint(x: ox + paperCssW + 12, y: oy + paperCssH)); context.addLine(to: CGPoint(x: ox + paperCssW + 12 - mark, y: oy + paperCssH))
context.move(to: CGPoint(x: ox + paperCssW, y: oy + paperCssH + 12)); context.addLine(to: CGPoint(x: ox + paperCssW, y: oy + paperCssH + 12 - mark))
context.strokePath()
context.setFillColor(CGColor(gray: 1, alpha: 1))
context.fill(paperRect)
if let page = currentPage {
let geo = geometry(for: page)
let dest = CGRect(
x: ox + geo.dest.minX * fit,
y: oy + geo.dest.minY * fit,
width: geo.dest.width * fit,
height: geo.dest.height * fit
)
context.interpolationQuality = .none
context.setShouldAntialias(false)
context.setAllowsAntialiasing(false)
context.draw(page.cgImage, in: dest)
if showImageable {
let img = geo.imageable
context.setStrokeColor(NSColor.systemBlue.withAlphaComponent(0.55).cgColor)
context.setLineDash(phase: 0, lengths: [4, 4])
context.stroke(CGRect(
x: ox + img.minX * fit,
y: oy + img.minY * fit,
width: img.width * fit,
height: img.height * fit
))
context.setLineDash(phase: 0, lengths: [])
context.stroke(dest)
}
}
if let warning = airPrintWarning, !warning.isEmpty {
drawWarningBanner(context: context, text: warning)
}
}
private func drawWarningBanner(context: CGContext, text: String) {
let banner = CGRect(x: 16, y: 16, width: bounds.width - 32, height: 52)
context.setFillColor(NSColor.systemOrange.withAlphaComponent(0.18).cgColor)
context.fill(banner)
context.setStrokeColor(NSColor.systemOrange.cgColor)
context.stroke(banner)
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: 12, weight: .medium),
.foregroundColor: NSColor.labelColor,
]
let str = NSAttributedString(string: "Warning: " + text, attributes: attrs)
str.draw(in: banner.insetBy(dx: 10, dy: 8))
}
}