Stand up the window architecture — welcome, board, card
Four scenes (welcome, restore bootstrap, board group, card group) with system restoration disabled in favor of the registry's open-now flags: set when a window actually opens, cleared only on user close, so quit — and crash — leave exactly the restoration set behind. AppModel joins windows to sessions (shared store, registry record, card refs, held security scope); CloseFlushCoordinator pins 02's strict close order as a seam-injected machine (card sessions end, windows drain, store flushes, record stamps, teardown) with named slots where m6/m7 flushes land. HostedWindowController proxies — never replaces — SwiftUI's window delegate to intercept windowShouldClose for the flush, report frames, and place saved frames onto live screens. Card windows are (board path, case-folded card id) values: reopen focuses, and a snapshot-pure fate function dismisses on delete, tombstone, tombstoned lane, or cross-board move. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
+117
-4
@@ -1,12 +1,125 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The scene graph (02-architecture.md § Windows, § Launch and window lifecycle).
|
||||
///
|
||||
/// ### Four scenes, and why each is the kind it is
|
||||
///
|
||||
/// - **Welcome** is a `Window`: there is one of it, ever, and `openWindow(id:)` focuses the existing
|
||||
/// one rather than making a second.
|
||||
/// - **The restore bootstrap** is a `Window` too, and a deliberate oddity — see
|
||||
/// `RestoreBootstrapView` for why launch-time work has to wear a window at all.
|
||||
/// - **Boards** and **cards** are `WindowGroup(for:)`s, because their identity is a *value*: opening
|
||||
/// with a ref that already has a window focuses it, which is how "one board window per root" and
|
||||
/// "at most one card window per card (reopen focuses)" are enforced by the scene rather than by
|
||||
/// bookkeeping.
|
||||
///
|
||||
/// ### Restoration is the registry's, not the system's
|
||||
///
|
||||
/// Both groups declare `.restorationBehavior(.disabled)`. The app already knows which boards were
|
||||
/// open — the registry's open-now flags, which survive a crash and reopen in `lastOpened` order —
|
||||
/// and letting AppKit *also* restore windows would produce duplicates, and worse, card windows
|
||||
/// restored behind boards that never opened. One mechanism, and it is the one that can explain
|
||||
/// itself when a board has moved or gone.
|
||||
///
|
||||
/// ### Which window appears at launch
|
||||
///
|
||||
/// Exactly one of welcome and the bootstrap, decided once in `init` and never re-derived: the
|
||||
/// preference is read before any scene exists, and `restorables()` costs a bookmark resolution per
|
||||
/// known board — a computed property here would pay that on every scene-graph evaluation.
|
||||
@main
|
||||
struct KanbanApp: App {
|
||||
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||
|
||||
@State private var appModel: AppModel
|
||||
|
||||
/// Whether this launch restores boards: the preference is on **and** there is something flagged
|
||||
/// to restore. Welcome "appears only when nothing restores".
|
||||
private let shouldRestoreAtLaunch: Bool
|
||||
|
||||
init() {
|
||||
let model = AppModel()
|
||||
_appModel = State(initialValue: model)
|
||||
shouldRestoreAtLaunch = AppPreferences.restoreOpenBoardsAtLaunch
|
||||
&& !model.boardRegistry.restorables().isEmpty
|
||||
// The delegate is constructed by the adaptor before this runs, so this is the one place the
|
||||
// app's model and its AppKit half meet.
|
||||
appDelegate.appModel = model
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
Text("Lanework")
|
||||
.font(.largeTitle)
|
||||
.padding(80)
|
||||
Window("Welcome to Lanework", id: WindowID.welcome) {
|
||||
WelcomeView()
|
||||
.environment(appModel)
|
||||
.captureWindowActions(into: appModel)
|
||||
}
|
||||
.defaultLaunchBehavior(shouldRestoreAtLaunch ? .suppressed : .automatic)
|
||||
.restorationBehavior(.disabled)
|
||||
.windowResizability(.contentSize)
|
||||
// Its automatic Window-menu item is replaced by the explicit command below, so the title is
|
||||
// the one 11-command-nexus.md names rather than whatever the scene happens to be called.
|
||||
.commandsRemoved()
|
||||
|
||||
Window("", id: WindowID.restoreBootstrap) {
|
||||
RestoreBootstrapView()
|
||||
.environment(appModel)
|
||||
.captureWindowActions(into: appModel)
|
||||
}
|
||||
.defaultLaunchBehavior(shouldRestoreAtLaunch ? .presented : .suppressed)
|
||||
.restorationBehavior(.disabled)
|
||||
.windowStyle(.plain)
|
||||
.defaultSize(width: 1, height: 1)
|
||||
.commandsRemoved()
|
||||
|
||||
WindowGroup(id: WindowID.board, for: BoardWindowRef.self) { $ref in
|
||||
if let ref {
|
||||
BoardWindowHost(ref: ref)
|
||||
.environment(appModel)
|
||||
.captureWindowActions(into: appModel)
|
||||
}
|
||||
}
|
||||
.restorationBehavior(.disabled)
|
||||
.defaultLaunchBehavior(.suppressed)
|
||||
.commands { menuCommands }
|
||||
|
||||
WindowGroup(id: WindowID.card, for: CardWindowRef.self) { $ref in
|
||||
if let ref {
|
||||
CardWindowHost(ref: ref)
|
||||
.environment(appModel)
|
||||
.captureWindowActions(into: appModel)
|
||||
}
|
||||
}
|
||||
.restorationBehavior(.disabled)
|
||||
.defaultLaunchBehavior(.suppressed)
|
||||
|
||||
Settings {
|
||||
SettingsView()
|
||||
.environment(appModel)
|
||||
.captureWindowActions(into: appModel)
|
||||
}
|
||||
}
|
||||
|
||||
/// The two menu items this milestone owns.
|
||||
///
|
||||
/// **The titles are API** (04-interactions.md ▸ Configurable bindings): macOS's App Shortcuts
|
||||
/// mechanism remaps menu items *by title*, so these strings are the keys a user's custom binding
|
||||
/// is stored under. They are spelled exactly as 11-command-nexus.md inventories them, and
|
||||
/// changing one silently breaks every remap of it.
|
||||
@CommandsBuilder
|
||||
private var menuCommands: some Commands {
|
||||
CommandGroup(after: .newItem) {
|
||||
Button("Open…") {
|
||||
appModel.presentOpenPanel()
|
||||
}
|
||||
.keyboardShortcut("o", modifiers: .command)
|
||||
}
|
||||
|
||||
CommandGroup(after: .windowList) {
|
||||
// No default chord — "— (no default)" in the Nexus is deliberate, not a gap; it remaps
|
||||
// like any other item.
|
||||
Button("Welcome to Lanework") {
|
||||
appModel.showWelcome()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user