Frontend Components: Unit 1 — Core Editor Engine
WYSIWYGTextView (NSViewRepresentable)
Interaction Model
SwiftUI Layer AppKit Layer
+------------------+ +------------------------+
| EditorView | | ShoechooTextView |
| (SwiftUI) | wraps | (NSTextView subclass)|
| | ---------> | |
| @State viewModel | | TextKit 2 stack: |
| settings | | NSTextContentStorage |
+------------------+ | NSTextLayoutManager |
| | NSTextContainer |
v +------------------------+
WYSIWYGTextView |
(NSViewRepresentable) v
makeNSView() ----> ShoechooTextView
updateNSView() --> sync state
Coordinator <----- delegate callbacks
ShoechooTextView Responsibilities
| Responsibility | Mechanism |
|---|---|
| Text editing | NSTextView built-in (TextKit 2) |
| IME input | NSTextInputClient (built-in) |
| Undo/Redo | NSUndoManager (built-in via NSTextView) |
| Spell check | NSSpellChecker (built-in) |
| Selection | NSTextView built-in |
| Cursor tracking | NSTextViewDelegate.textViewDidChangeSelection() |
| Text changes | NSTextStorageDelegate.textStorage(_:didProcessEditing:...) |
| Drag & drop images | performDragOperation(_:) override |
| Focus mode dimming | Custom drawing via NSTextLayoutFragment alpha |
| Typewriter scrolling | scrollRangeToVisible() with center offset |
NSViewRepresentable Lifecycle
struct WYSIWYGTextView: NSViewRepresentable {
@Bindable var viewModel: EditorViewModel
var settings: EditorSettings
func makeNSView(context: Context) -> ShoechooTextView {
// 1. Create NSTextContentStorage + NSTextLayoutManager + NSTextContainer
// 2. Create ShoechooTextView with TextKit 2 stack
// 3. Configure: editable, richText=false, allowsUndo
// 4. Set delegate to Coordinator
// 5. Apply initial attributed string from viewModel
return textView
}
func updateNSView(_ nsView: ShoechooTextView, context: Context) {
// Called when SwiftUI state changes
// 1. If viewModel.needsFullRerender: replace entire attributed string
// 2. If viewModel.changedBlockIDs: update only those ranges
// 3. Apply focus mode dimming if enabled
// 4. Apply typewriter scroll if enabled
// 5. Sync font/spacing from settings
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
}
Coordinator (Delegate Bridge)
class Coordinator: NSObject, NSTextViewDelegate, NSTextStorageDelegate {
var parent: WYSIWYGTextView
// Text change → ViewModel
func textStorage(_ ts: NSTextStorage, didProcessEditing ...) {
parent.viewModel.textDidChange(ts.string, editedRange: editedRange)
}
// Cursor move → ViewModel
func textViewDidChangeSelection(_ notification: Notification) {
let pos = textView.selectedRange().location
parent.viewModel.cursorDidMove(to: pos)
}
// IME composition tracking
// Detected via textView.hasMarkedText()
}
User Interaction Flows
Flow 1: Typing Text
1. User types character
2. NSTextView inserts character (TextKit 2)
3. NSTextStorageDelegate fires
4. Coordinator calls viewModel.textDidChange()
5. ViewModel increments revision, schedules parse (50ms debounce)
6. Parse completes → diff → selective re-render
7. updateNSView() applies changed block attributed strings
Flow 2: Moving Cursor (Arrow Keys / Click)
1. User clicks or arrow-keys to new position
2. NSTextViewDelegate.textViewDidChangeSelection fires
3. Coordinator calls viewModel.cursorDidMove(to:)
4. ViewModel resolves new active block
5. If changed: re-render old block (styled) + new block (raw syntax)
6. updateNSView() applies changes
Flow 3: Keyboard Shortcut (Cmd+B)
1. User selects text, presses Cmd+B
2. Menu/key handler in ShoechooTextView or EditorView
3. ViewModel toggles bold: wraps/unwraps selection with **
4. Source text modified → normal text change flow
Flow 4: Task List Checkbox Click
1. User clicks checkbox in inactive (rendered) task list
2. ShoechooTextView detects click on checkbox region
3. Toggle: find source range of [ ] or [x], swap
4. Source text modified → normal text change flow
Flow 5: IME Composition (Japanese Input)
1. User starts IME input (marked text appears)
2. ShoechooTextView.hasMarkedText() == true
3. ViewModel pauses parse pipeline (BR-03.1)
4. User continues composing (underlined preview text)
5. User commits composition (Enter/select candidate)
6. hasMarkedText() == false
7. ViewModel resumes parse pipeline immediately
Keyboard Shortcut Registration
// In EditorView or ShoechooApp commands
.commands {
CommandGroup(replacing: .textFormatting) {
Button("Bold") { viewModel.toggleBold() }
.keyboardShortcut("b", modifiers: .command)
Button("Italic") { viewModel.toggleItalic() }
.keyboardShortcut("i", modifiers: .command)
Button("Link") { viewModel.insertLink() }
.keyboardShortcut("k", modifiers: .command)
Button("Inline Code") { viewModel.toggleInlineCode() }
.keyboardShortcut("k", modifiers: [.command, .shift])
}
CommandGroup(after: .textFormatting) {
ForEach(1...6, id: \.self) { level in
Button("Heading \(level)") { viewModel.setHeading(level: level) }
.keyboardShortcut(KeyEquivalent(Character("\(level)")), modifiers: .command)
}
}
}