The board window says its name once — the scene declares the title SwiftUI keeps hiding
`HostedWindowController.hideTitle()` was the same shape of buga429a7efixed for `titlebarAppearsTransparent`: an out-of-band `NSWindow.titleVisibility` write, correct the instant it ran, undone by SwiftUI's own next pass over the window's configuration — a tree that declares nothing resolves `.visible`, and SwiftUI writes that back over the out-of-band `.hidden` on the very next `@State`-driven re-render this board window's own liveness causes. The system title reappeared beside the board-popover widget, "occasionally" — whenever that next re-render happened to land. Confirmed with an A/B harness (no interactive display in this session, so not reproduced on screen; mechanism established in code, per the card's own fallback): a bare out-of-band write held indefinitely against resize and key-status changes alone, but reverted on the very next `@State`-driven render and stayed reverted — reasserting from `body`'s own construction or from `.onChange` both lost the same race, since SwiftUI's resync runs later than either. The only thing that held was declaring the posture in the tree itself, mirroring `.toolbarBackgroundVisibility`'s role ina429a7e. `KanbanApp`'s board `WindowGroup` now declares `.windowToolbarStyle(.unified(showsTitle: false))`. It is a scene modifier, not a per-window one, so — unlike `.toolbarBackgroundVisibility` — it cannot wait for a board's load to finish before taking effect; every board window it creates keeps the system title hidden from its very first frame. `boardLoadingTitlebarAccessory` covers the gap that opens before the loading window has a store to build the real widget from: a small, non-interactive, plain-text stand-in carrying the registry record's cached name, installed the moment the window attaches and swapped by identity for the real widget the moment the store loads — so the loading window's chrome still carries a name throughout, per 02-architecture.md. `hideTitle()`'s own write stays; it is no longer what keeps the title hidden, but it is still correct for the one render turn before the scene's own re-assertion catches up. Confined to `BoardWindowHost.swift`, `BoardInfoPopover.swift` and `KanbanApp.swift` — `WindowAccessor.swift`'s shared `hideTitle()`/`titleVisibility` machinery is untouched, since a concurrent fix is addressing the card window's version of this same bug through that file. New regression tests (`BoardLoadingTitlebarStandInTests`, `KanbanTests/BoardLoadingTests.swift`) pin the stand-in's layout and the identity-based swap. Full suite green (3219 tests) except the pre-existing, documented environment-sensitive `PointerLatencyTests`, confirmed unaffected by rerunning them in isolation. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -249,6 +249,47 @@ func boardInfoTitlebarAccessory(
|
||||
return controller
|
||||
}
|
||||
|
||||
/// The widget's stand-in for the board window's brief life before it has a store — the loading
|
||||
/// window's own name (`BoardWindowHost.loadingTitle`), styled to match the real widget's title
|
||||
/// line, with no icon (the registry record carries no cached glyph, `BoardRecord`), no chevron and
|
||||
/// no popover: there is nothing to open yet.
|
||||
///
|
||||
/// **Why the loading window needs any widget at all**, now that `KanbanApp`'s board scene declares
|
||||
/// `.windowToolbarStyle(.unified(showsTitle: false))` unconditionally: that modifier is the scene's,
|
||||
/// not this window's, so it cannot wait for the store the way `HostedWindowController.hideTitle()`
|
||||
/// used to — every board window it creates keeps the system title hidden from its very first frame,
|
||||
/// loading or not. Leaving the loading window with no name anywhere in its chrome would be exactly
|
||||
/// what 02-architecture.md's "its chrome carrying the registry record's cached title" rules out, so
|
||||
/// `BoardWindowHost.configureLoadingWindow` installs this in the system title's place and
|
||||
/// `configureWindow` swaps it for `boardInfoTitlebarAccessory` the moment the real one exists.
|
||||
@MainActor
|
||||
func boardLoadingTitlebarAccessory(title: String) -> NSTitlebarAccessoryViewController {
|
||||
let hosting = NSHostingView(
|
||||
rootView: HStack(spacing: 6) {
|
||||
// A glyph-shaped gap rather than the glyph itself — there is no cached icon to draw
|
||||
// (`BoardRecord` carries none), but leaving the text flush left would have it jump right
|
||||
// by the icon's width at the swap. 22pt is `BoardInfoWidget`'s own icon size.
|
||||
Color.clear.frame(width: 22, height: 22)
|
||||
Text(title)
|
||||
// The same line the real widget draws (`BoardInfoWidget.body`), so the swap to the
|
||||
// interactive widget reads as a content change close to in place, not a jump.
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
.frame(maxWidth: 400, alignment: .leading)
|
||||
.frame(height: 32)
|
||||
)
|
||||
hosting.sizingOptions = [.intrinsicContentSize]
|
||||
hosting.frame = NSRect(x: 0, y: 0, width: 200, height: 32)
|
||||
|
||||
let controller = NSTitlebarAccessoryViewController()
|
||||
controller.view = hosting
|
||||
controller.layoutAttribute = .leading
|
||||
return controller
|
||||
}
|
||||
|
||||
// MARK: - Tabs
|
||||
|
||||
/// The popover's aspects, one tab each (03-board-ui.md § Board popover, the 2026-08-07 tab
|
||||
|
||||
Reference in New Issue
Block a user