Files
calctext/calcpad-macos/Sources/CalcPad/Views/AnswerColumnView.swift
C. Cassel 0d38bd3108 feat(web): implement complete workspace with themes, tabs, sidebar, and mobile
Transform CalcText from a single-document calculator into a full workspace
application with multi-document support, theming, and responsive mobile experience.

- Theme system: 5 presets (Light, Dark, Matrix, Midnight, Warm) + accent colors
- Document model with localStorage persistence and auto-save
- Tab bar with keyboard shortcuts (Ctrl+N/W/Tab/1-9), rename, close
- Sidebar with search, recent, favorites, folders, templates, drag-and-drop
- 5 templates: Budget, Invoice, Unit Converter, Trip Planner, Loan Calculator
- Status bar with cursor position, engine status, dedication to Igor Cassel
- Results panel: type-specific colors, click-to-copy, error hints
- Format toolbar: H, B, I, //, color labels with live preview toggle
- Syntax highlighting using theme CSS variables
- Error hover tooltips
- Mobile: bottom results tray, sidebar drawer, touch targets, safe areas
- Docker multi-stage build (Rust WASM + Vite + Nginx)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:12:05 -04:00

114 lines
4.0 KiB
Swift

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?
}
}