import AppKit import SwiftUI /// Displays calculation results in a vertical column, one result per line, /// aligned to match the corresponding editor lines. /// Uses NSViewRepresentable wrapping NSScrollView + StripedTextView for /// pixel-perfect line height alignment with the editor, including zebra striping. struct AnswerColumnView: NSViewRepresentable { let results: [LineResult] @Binding var scrollOffset: CGFloat var font: NSFont var alignment: NSTextAlignment = .right func makeCoordinator() -> Coordinator { Coordinator() } func makeNSView(context: Context) -> NSScrollView { let scrollView = NSScrollView() scrollView.hasVerticalScroller = false scrollView.hasHorizontalScroller = false scrollView.borderType = .noBorder scrollView.drawsBackground = false // Disable user scrolling — scroll is driven by the editor scrollView.verticalScrollElasticity = .none scrollView.horizontalScrollElasticity = .none let textView = StripedTextView() textView.isEditable = false textView.isSelectable = true textView.isRichText = true textView.usesFontPanel = false textView.drawsBackground = true textView.backgroundColor = .clear // Match editor text container settings for alignment textView.textContainer?.lineFragmentPadding = 4 textView.textContainerInset = NSSize(width: 8, height: 8) // Let the text container track the scroll view width textView.isHorizontallyResizable = false textView.textContainer?.widthTracksTextView = true textView.textContainer?.containerSize = NSSize( width: 0, height: CGFloat.greatestFiniteMagnitude ) textView.maxSize = NSSize( width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude ) scrollView.documentView = textView context.coordinator.textView = textView context.coordinator.scrollView = scrollView updateContent(textView: textView) return scrollView } func updateNSView(_ scrollView: NSScrollView, context: Context) { guard let textView = scrollView.documentView as? NSTextView else { return } updateContent(textView: textView) // Sync scroll position from editor let currentOffset = scrollView.contentView.bounds.origin.y if abs(currentOffset - scrollOffset) > 0.5 { scrollView.contentView.scroll(to: NSPoint(x: 0, y: scrollOffset)) scrollView.reflectScrolledClipView(scrollView.contentView) } } private func updateContent(textView: NSTextView) { let attributedString = NSMutableAttributedString() let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = alignment let resultColor = NSColor.secondaryLabelColor let errorColor = NSColor.systemRed for (index, lineResult) in results.enumerated() { // Only show successful results — errors stay as underlines in the editor let displayText = lineResult.isError ? "" : (lineResult.result ?? "") let color = resultColor let attributes: [NSAttributedString.Key: Any] = [ .font: font, .foregroundColor: color, .paragraphStyle: paragraphStyle, ] let line = NSAttributedString(string: displayText, attributes: attributes) attributedString.append(line) // Add newline between lines (but not after the last) if index < results.count - 1 { let newline = NSAttributedString( string: "\n", attributes: [.font: font] ) attributedString.append(newline) } } textView.textStorage?.setAttributedString(attributedString) } final class Coordinator { weak var textView: NSTextView? weak var scrollView: NSScrollView? } }