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 { 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() } } } }