Phase 4 — Platform shells: - calcpad-macos/: SwiftUI two-column editor with Rust FFI bridge (16 files) - calcpad-windows/: iced GUI with Windows 11 Fluent theme (7 files, 13 tests) - calcpad-web/: React 18 + CodeMirror 6 + WASM Worker + PWA (20 files) - calcpad-cli/: clap-based CLI with expression eval, pipe/stdin, JSON/CSV output, and interactive REPL with rustyline history Phase 5 — Engine modules: - formatting/: answer formatting (decimal/scientific/SI notation, thousands separators, currency), line type classification, clipboard values (93 tests) - plugins/: CalcPadPlugin trait, PluginRegistry, Rhai scripting stub (43 tests) - benches/: criterion benchmarks (single-line, 100/500-line sheets, DAG, incremental) - tests/sheet_scenarios.rs: 20 real-world integration tests - tests/proptest_fuzz.rs: 12 property-based fuzz tests 771 tests passing across workspace, 0 failures.
73 lines
2.6 KiB
Swift
73 lines
2.6 KiB
Swift
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.
|
|
struct TwoColumnEditorView: View {
|
|
@State private var text: String = ""
|
|
@State private var scrollOffset: CGFloat = 0
|
|
@State private var results: [LineResult] = []
|
|
@State private var evaluationTask: Task<Void, Never>?
|
|
|
|
/// 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 {
|
|
// Use the system's preferred monospaced font size, which scales with
|
|
// Accessibility > Display > Text Size in System Settings (macOS 14+).
|
|
let baseSize = NSFont.systemFontSize
|
|
// Scale with accessibility settings via the body text style size
|
|
let preferredSize = NSFont.preferredFont(forTextStyle: .body, options: [:]).pointSize
|
|
// Use the larger of system default or accessibility-preferred size
|
|
let size = max(baseSize, preferredSize)
|
|
return NSFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
|
}
|
|
|
|
var body: some View {
|
|
HSplitView {
|
|
// Left pane: Editor
|
|
EditorTextView(
|
|
text: $text,
|
|
scrollOffset: $scrollOffset,
|
|
font: editorFont
|
|
)
|
|
.frame(minWidth: 200)
|
|
|
|
// Divider is automatic with HSplitView
|
|
|
|
// Right pane: Answer column
|
|
AnswerColumnView(
|
|
results: results,
|
|
scrollOffset: $scrollOffset,
|
|
font: editorFont
|
|
)
|
|
.frame(minWidth: 120, idealWidth: 200)
|
|
}
|
|
.onChange(of: text) { _, newValue in
|
|
scheduleEvaluation(newValue)
|
|
}
|
|
.onAppear {
|
|
evaluateText(text)
|
|
}
|
|
}
|
|
|
|
/// Debounce evaluation so rapid typing doesn't cause excessive recalculation.
|
|
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)
|
|
}
|
|
}
|