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:
@@ -0,0 +1,79 @@
|
||||
import SwiftUI
|
||||
import os
|
||||
|
||||
/// The launch-time restoration pass, wearing a window because that is the only place SwiftUI lets
|
||||
/// work like this run.
|
||||
///
|
||||
/// ### Why a window at all
|
||||
///
|
||||
/// Restoration has to open windows, and opening a window needs `openWindow`, which is only readable
|
||||
/// from a view. An `App.init()` cannot do it and `AppDelegate` has no environment. So the app
|
||||
/// presents one throwaway window at launch — 1×1, plain, ordered straight back out, absent from the
|
||||
/// Window menu — whose only job is to run the pass and then dismiss itself. It exists for a few
|
||||
/// hundred milliseconds and never draws.
|
||||
///
|
||||
/// It is presented **only** when there is something to restore (`KanbanApp` decides), so the ordinary
|
||||
/// launch-to-welcome path never creates it.
|
||||
///
|
||||
/// ### What the pass does
|
||||
///
|
||||
/// Reads the registry's flagged records in `lastOpened` order (`BoardRegistry.restorables()`), opens
|
||||
/// the available ones, and records the unavailable ones as failures — 02 § Launch and window
|
||||
/// lifecycle: "Other restorations proceed unaffected — never a launch-time modal chain, never a
|
||||
/// silent drop." Welcome comes up only if nothing was even attempted; a board that *was* attempted
|
||||
/// and then failed to load opens welcome from its own host, which is the same rule applied one layer
|
||||
/// down and keeps this pass from having to wait on loads it did not perform.
|
||||
struct RestoreBootstrapView: View {
|
||||
|
||||
@Environment(AppModel.self) private var appModel
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@Environment(\.dismissWindow) private var dismissWindow
|
||||
|
||||
@State private var windowController = HostedWindowController()
|
||||
|
||||
private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "launch")
|
||||
|
||||
var body: some View {
|
||||
Color.clear
|
||||
.frame(width: 1, height: 1)
|
||||
.background(WindowAccessor(controller: windowController))
|
||||
.onAppear {
|
||||
// Out of sight before it can be seen. `orderOut` rather than a hidden style because
|
||||
// the scene must still exist — a window SwiftUI never presents never runs its task.
|
||||
windowController.onAttach = { window in
|
||||
window.alphaValue = 0
|
||||
window.orderOut(nil)
|
||||
}
|
||||
if let window = windowController.window {
|
||||
windowController.onAttach?(window)
|
||||
}
|
||||
}
|
||||
.task { await restore() }
|
||||
}
|
||||
|
||||
private func restore() async {
|
||||
// Captured directly rather than waiting for `CaptureOpenWindow`'s `onAppear`: this task is
|
||||
// the app's first act, and `openBoard` needs the action now.
|
||||
appModel.captureWindowActions(open: openWindow, dismiss: dismissWindow)
|
||||
|
||||
var attempted = 0
|
||||
for board in appModel.boardRegistry.restorables() {
|
||||
switch board {
|
||||
case let .available(_, url):
|
||||
appModel.openBoard(at: url)
|
||||
attempted += 1
|
||||
case let .unavailable(record):
|
||||
Self.logger.error("a flagged board could not be restored — its bookmark no longer resolves")
|
||||
appModel.recordLaunchFailure(
|
||||
path: record.lastKnownPath,
|
||||
message: "This board is unavailable. Its volume may be offline, or it may have been moved or deleted."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if attempted == 0 {
|
||||
appModel.showWelcome()
|
||||
}
|
||||
dismissWindow(id: WindowID.restoreBootstrap)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user