Build Preview mode rendering
The card body's resting state: swift-markdown (pinned 0.8.0, smart typography off — Preview renders the bytes on disk) parsed into a pure BodyMarkup model with UTF-8 source offsets, rendered on one hosted TextKit 1 NSTextView — chosen because find-in-text is NSTextFinder, checkbox clicks reuse AppKit character hit-testing, links are .link attributes, and NSTextTable's automatic layout is exactly the columns-sized-to-contents rule. The GFM subset renders per 05; HTML stays verbatim code-styled text; relative images resolve against the card folder while remote URLs are never fetched, drawing a quiet chip instead. Task checkboxes are live: a click flips exactly one byte through a fresh-read, refuse-uneditable, stamp, atomic-replace write — the app's only offset-addressed write, so a moved target refuses as staleTarget and what the user saw decides the direction, netting one toggle on a double-click. Empty bodies open in Edit per CardBodyMode's opening rule, applied once; the Edit surface itself stays an honest read-only stub until its card. FindCommand prefers the card body's find over board search when a card window is focused. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import Observation
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - The mode
|
||||
|
||||
/// Which of the body column's two surfaces is showing (05-card-window.md ▸ Mode grammar).
|
||||
///
|
||||
/// **Two cases, not three.** The raw-source outlet swaps the *entire content area* — title, body
|
||||
/// and sidebar — so it is a state of the window, not of the body column, and it does not belong in
|
||||
/// this enum. Edit Body disabling while raw source is active (11-command-nexus.md) is that
|
||||
/// window-level state's rule to enforce over this one.
|
||||
public enum CardBodyMode: Equatable, Sendable {
|
||||
/// The rendered, selectable preview — **the resting state**.
|
||||
case preview
|
||||
/// The raw-Markdown editor.
|
||||
case edit
|
||||
|
||||
/// The mode a window opens its body in: **Preview, unless the body is empty** (05 ▸ Mode
|
||||
/// grammar: "a card opens in Preview — unless its body is empty, which opens straight into
|
||||
/// Edit with the cursor ready (a new card has nothing to preview, so ⌘↩ during creation flows
|
||||
/// title → body without a mode stop)").
|
||||
///
|
||||
/// Whitespace is empty (`BodyMarkup.isEmpty`): a body holding one newline previews as a blank
|
||||
/// page, and stopping the user at a blank preview of a blank body is precisely the ceremony
|
||||
/// the rule removes.
|
||||
public static func opening(body: String) -> CardBodyMode {
|
||||
BodyMarkup.isEmpty(body) ? .edit : .preview
|
||||
}
|
||||
|
||||
/// ⌘E — View ▸ Edit Body's checkmark toggle. Also the whole of "Return in Preview enters Edit"
|
||||
/// and "Escape in Edit returns to Preview": three gestures, one flip, so they cannot drift.
|
||||
public var toggled: CardBodyMode {
|
||||
self == .preview ? .edit : .preview
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The window's body surface, as a handle
|
||||
|
||||
/// One card window's body column, reduced to what things *outside* it need: which mode it is in,
|
||||
/// and how to put a find bar over whichever surface currently holds the keyboard.
|
||||
///
|
||||
/// `BoardSearchPresentation`'s shape and for its reason — one per window, `@State` in the host,
|
||||
/// published through the focus system so a **menu item** (Edit ▸ Find, ⌘F) can reach the frontmost
|
||||
/// card window without anyone keeping a which-window-is-key register. It is deliberately not on
|
||||
/// `BoardStore`: the store is the *board's*, shared by every window on it, and two card windows
|
||||
/// open on two cards of one board are in two different modes.
|
||||
@MainActor
|
||||
@Observable
|
||||
public final class CardBodyPresentation {
|
||||
|
||||
/// The surface showing right now. Starts in Preview and is settled by `openIfNeeded(body:)`
|
||||
/// the first time the window has a body to judge.
|
||||
public var mode: CardBodyMode = .preview
|
||||
|
||||
/// Puts the standard find bar over the focused body surface — **Edit ▸ Find (⌘F) is
|
||||
/// find-in-text here** (05 ▸ Preview; 11-command-nexus.md scopes ⌘F "Board window: board
|
||||
/// search; card window: find-in-text"). Filled in by the surface itself, which is the only
|
||||
/// thing that holds a text view to hand the action to; `nil` until one exists, which is also
|
||||
/// exactly when ⌘F has nothing to find in.
|
||||
public var findInText: (() -> Void)?
|
||||
|
||||
/// Whether the opening rule has already run for this window.
|
||||
///
|
||||
/// **Once, not per snapshot.** The rule is about *opening* a card, and the body it judges
|
||||
/// arrives with the first snapshot — but snapshots keep arriving (a watcher reload, a lane
|
||||
/// move, another window's edit). Re-running it would drag a reader back into Edit the moment
|
||||
/// someone else emptied the file, and would fight a user who had just pressed ⌘E.
|
||||
private var hasOpened = false
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Applies the opening rule the first time it is called, and does nothing on every call after.
|
||||
@discardableResult
|
||||
public func openIfNeeded(body: String) -> CardBodyMode {
|
||||
guard !hasOpened else { return mode }
|
||||
hasOpened = true
|
||||
mode = CardBodyMode.opening(body: body)
|
||||
return mode
|
||||
}
|
||||
|
||||
/// ⌘E, Return in Preview, Escape in Edit — see `CardBodyMode.toggled`.
|
||||
public func toggleMode() {
|
||||
mode = mode.toggled
|
||||
}
|
||||
}
|
||||
|
||||
/// The focused card window's body column, beside `FocusedValues.boardSearch` — see
|
||||
/// `FocusedBoardStoreKey` for why window-scoped menu items reach their window this way.
|
||||
struct FocusedCardBodyKey: FocusedValueKey {
|
||||
typealias Value = CardBodyPresentation
|
||||
}
|
||||
|
||||
extension FocusedValues {
|
||||
var cardBody: CardBodyPresentation? {
|
||||
get { self[FocusedCardBodyKey.self] }
|
||||
set { self[FocusedCardBodyKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user