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>
132 lines
5.3 KiB
Swift
132 lines
5.3 KiB
Swift
import AppKit
|
|
import Combine
|
|
import SwiftUI
|
|
|
|
/// The main two-column editor layout: text editor on the left, results on the right.
|
|
/// Scrolling is synchronized between both columns.
|
|
/// Includes a toolbar for justification toggles and alternating row striping.
|
|
struct TwoColumnEditorView: View {
|
|
@State private var text: String = "# CalcPad\n\n// Basic arithmetic\n2 + 3\n10 * 4.5\n100 / 7\n\n// Variables\nprice = 49.99\nquantity = 3\nsubtotal = price * quantity\n\n// Percentages\ntax = subtotal * 8%\ntotal = subtotal + tax\n\n// Functions\nsqrt(144)\n2 ^ 10\n"
|
|
@State private var scrollOffset: CGFloat = 0
|
|
@State private var results: [LineResult] = []
|
|
@State private var evaluationTask: Task<Void, Never>?
|
|
@State private var editorAlignment: NSTextAlignment = .left
|
|
@State private var resultsAlignment: NSTextAlignment = .right
|
|
|
|
/// Uses the Rust FFI engine. Falls back to StubCalculationEngine if the Rust
|
|
/// library is not linked (e.g., during UI-only development).
|
|
private let engine: CalculationEngine = RustCalculationEngine()
|
|
|
|
/// Debounce interval for re-evaluation after typing (seconds).
|
|
private let evaluationDebounce: TimeInterval = 0.05
|
|
|
|
/// Font that respects the user's accessibility / Dynamic Type settings.
|
|
private var editorFont: NSFont {
|
|
let baseSize = NSFont.systemFontSize
|
|
let preferredSize = NSFont.preferredFont(forTextStyle: .body, options: [:]).pointSize
|
|
let size = max(baseSize, preferredSize)
|
|
return NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Justification toolbar
|
|
HStack(spacing: 0) {
|
|
// Editor alignment buttons
|
|
HStack(spacing: 4) {
|
|
Button(action: { editorAlignment = .left }) {
|
|
Image(systemName: "text.alignleft")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.foregroundColor(editorAlignment == .left ? .accentColor : .secondary)
|
|
.help("Align editor text left")
|
|
|
|
Button(action: { editorAlignment = .center }) {
|
|
Image(systemName: "text.aligncenter")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.foregroundColor(editorAlignment == .center ? .accentColor : .secondary)
|
|
.help("Align editor text center")
|
|
|
|
Button(action: { editorAlignment = .right }) {
|
|
Image(systemName: "text.alignright")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.foregroundColor(editorAlignment == .right ? .accentColor : .secondary)
|
|
.help("Align editor text right")
|
|
}
|
|
.padding(.horizontal, 8)
|
|
|
|
Spacer()
|
|
|
|
// Results alignment buttons
|
|
HStack(spacing: 4) {
|
|
Button(action: { resultsAlignment = .left }) {
|
|
Image(systemName: "text.alignleft")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.foregroundColor(resultsAlignment == .left ? .accentColor : .secondary)
|
|
.help("Align results left")
|
|
|
|
Button(action: { resultsAlignment = .center }) {
|
|
Image(systemName: "text.aligncenter")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.foregroundColor(resultsAlignment == .center ? .accentColor : .secondary)
|
|
.help("Align results center")
|
|
|
|
Button(action: { resultsAlignment = .right }) {
|
|
Image(systemName: "text.alignright")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.foregroundColor(resultsAlignment == .right ? .accentColor : .secondary)
|
|
.help("Align results right")
|
|
}
|
|
.padding(.horizontal, 8)
|
|
}
|
|
.padding(.vertical, 4)
|
|
|
|
Divider()
|
|
|
|
HSplitView {
|
|
// Left pane: Editor
|
|
EditorTextView(
|
|
text: $text,
|
|
scrollOffset: $scrollOffset,
|
|
font: editorFont,
|
|
alignment: editorAlignment
|
|
)
|
|
.frame(minWidth: 200)
|
|
|
|
// Right pane: Answer column
|
|
AnswerColumnView(
|
|
results: results,
|
|
scrollOffset: $scrollOffset,
|
|
font: editorFont,
|
|
alignment: resultsAlignment
|
|
)
|
|
.frame(minWidth: 120, idealWidth: 200)
|
|
}
|
|
}
|
|
.onChange(of: text) { _, newValue in
|
|
scheduleEvaluation(newValue)
|
|
}
|
|
.onAppear {
|
|
evaluateText(text)
|
|
}
|
|
}
|
|
|
|
private func scheduleEvaluation(_ newText: String) {
|
|
evaluationTask?.cancel()
|
|
evaluationTask = Task { @MainActor in
|
|
try? await Task.sleep(for: .milliseconds(Int(evaluationDebounce * 1000)))
|
|
guard !Task.isCancelled else { return }
|
|
evaluateText(newText)
|
|
}
|
|
}
|
|
|
|
private func evaluateText(_ newText: String) {
|
|
results = engine.evaluateSheet(newText)
|
|
}
|
|
}
|