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