The clear title bar is said in SwiftUI — a configuration pass stops painting the bar back over the board's picture
`titlebarAppearsTransparent` is not the app's to hold on a scene window: it is the AppKit face of the tree's resolved `toolbarBackgroundVisibility`, and SwiftUI writes it on every pass that re-applies a window's configuration. The board stated nothing, so `.automatic` resolved and each pass put `false` back over what `HostedWindowController` had set out of band — correct on open, because the walk lands after the pass that follows window creation, and opaque at the next one, with nothing to re-assert it since the board's own reading had not changed. The board now declares the posture where SwiftUI will keep asserting it; the AppKit write stays as the same value one turn earlier. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -171,6 +171,11 @@ struct BoardWindowHost: View {
|
|||||||
// Nothing to render and nothing worth animating: this window is dismissing itself.
|
// Nothing to render and nothing worth animating: this window is dismissing itself.
|
||||||
Color.clear
|
Color.clear
|
||||||
case let .open(store):
|
case let .open(store):
|
||||||
|
// **Whether this board paints a background of its own**, read once and spent twice
|
||||||
|
// below — the AppKit half of the chrome decision (`setExtendsContentUnderTitlebar`) and
|
||||||
|
// the SwiftUI half (`toolbarBackgroundVisibility`) are one answer, not two, and reading
|
||||||
|
// it twice would be the first step towards their disagreeing.
|
||||||
|
let custom = BoardBackdrop.isCustom(store.snapshot, root: store.rootURL)
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
BannerStripView(rows: store.bannerRows) { store.banners.dismiss($0) }
|
BannerStripView(rows: store.bannerRows) { store.banners.dismiss($0) }
|
||||||
// **⌘F's fallback**, and only that: the search field's home is the toolbar item
|
// **⌘F's fallback**, and only that: the search field's home is the toolbar item
|
||||||
@@ -221,9 +226,31 @@ struct BoardWindowHost: View {
|
|||||||
// first render is already a level, not a change — this is the board's first statement
|
// first render is already a level, not a change — this is the board's first statement
|
||||||
// about its chrome, and the loading half deliberately made none
|
// about its chrome, and the loading half deliberately made none
|
||||||
// (`HostedWindowController.extendsUnderTitlebar`).
|
// (`HostedWindowController.extendsUnderTitlebar`).
|
||||||
.onChange(of: BoardBackdrop.isCustom(store.snapshot, root: store.rootURL), initial: true) { _, custom in
|
.onChange(of: custom, initial: true) { _, custom in
|
||||||
windowController.setExtendsContentUnderTitlebar(custom)
|
windowController.setExtendsContentUnderTitlebar(custom)
|
||||||
}
|
}
|
||||||
|
// **And the same sentence said to SwiftUI, which is the half that lasts.**
|
||||||
|
//
|
||||||
|
// `NSWindow.titlebarAppearsTransparent` is not the app's property to hold on a SwiftUI
|
||||||
|
// scene window: it is the AppKit face of *this* modifier, and SwiftUI writes it — the
|
||||||
|
// resolved value, whether or not it changed — every time it re-applies a window's
|
||||||
|
// configuration. A tree that says nothing resolves `.automatic`, so every such pass
|
||||||
|
// wrote `false` over the `true` the line above had just set out of band. The board
|
||||||
|
// opened correct, because the load lands after the pass that follows window creation,
|
||||||
|
// and turned opaque at the next one — a title bar painting its own material and its
|
||||||
|
// `titlebarSeparatorStyle` hairline across a picture that is still being drawn full
|
||||||
|
// height underneath, which is exactly what the bug's screenshots show (nothing below
|
||||||
|
// the bar moves, because `.fullSizeContentView` was never the half that was lost).
|
||||||
|
//
|
||||||
|
// Stating it here makes SwiftUI's own passes assert the posture the board asked for
|
||||||
|
// instead of undoing it. Verified as an A/B against a real configuration pass: the same
|
||||||
|
// AppKit write survives with this modifier declared and is reverted within ~15 ms
|
||||||
|
// without it.
|
||||||
|
//
|
||||||
|
// The AppKit write stays, and is not a second owner of the same truth — it is the same
|
||||||
|
// value one turn earlier, so the frame in which a `background:` first reads is already
|
||||||
|
// drawn under a clear bar. What it cannot do is *stay* said; that is this line's job.
|
||||||
|
.toolbarBackgroundVisibility(Self.toolbarBackground(overCustomBackground: custom), for: .windowToolbar)
|
||||||
// (**The board settings sheet was presented here** between 2026-07-31 and 2026-08-07,
|
// (**The board settings sheet was presented here** between 2026-07-31 and 2026-08-07,
|
||||||
// when the popover/sheet split was reversed: the sheet retired, its contents rehomed
|
// when the popover/sheet split was reversed: the sheet retired, its contents rehomed
|
||||||
// into the popover's Git tab — itself retired 2026-08-08 with the git excision
|
// into the popover's Git tab — itself retired 2026-08-08 with the git excision
|
||||||
@@ -288,6 +315,23 @@ struct BoardWindowHost: View {
|
|||||||
record?.displayName ?? AppModel.folderDisplayName(of: url)
|
record?.displayName ?? AppModel.folderDisplayName(of: url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **What the window toolbar's background does over this board** — gone when the board paints
|
||||||
|
/// one of its own, the system's own answer when it does not (03-board-ui.md § Styling ▸
|
||||||
|
/// Capabilities: the board's colour or image "paints the full window").
|
||||||
|
///
|
||||||
|
/// `.automatic` rather than `.visible` for the ordinary board, deliberately: a board with no
|
||||||
|
/// background of its own must keep "the standard chrome exactly as it has always looked", and
|
||||||
|
/// only `.automatic` means *whatever the system would have done* — including the scroll-driven
|
||||||
|
/// appearance AppKit manages for itself. `.visible` would pin a bar on permanently and would be
|
||||||
|
/// this modifier's own version of the bug it exists to fix.
|
||||||
|
///
|
||||||
|
/// Static and pure for `loadingTitle`'s reason: this one line is the whole of the rule that a
|
||||||
|
/// board's chrome follows its background, and an inversion here is invisible in review and
|
||||||
|
/// obvious on screen.
|
||||||
|
static func toolbarBackground(overCustomBackground custom: Bool) -> Visibility {
|
||||||
|
custom ? .hidden : .automatic
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Opening
|
// MARK: - Opening
|
||||||
|
|
||||||
/// Starts the open as a task of its own, so something can hold it.
|
/// Starts the open as a task of its own, so something can hold it.
|
||||||
|
|||||||
@@ -285,6 +285,18 @@ final class HostedWindowController: NSObject, NSWindowDelegate {
|
|||||||
/// what stops the title bar from painting its own material over it. Either alone is a visible
|
/// what stops the title bar from painting its own material over it. Either alone is a visible
|
||||||
/// half-state — an opaque bar over the board, or a board that stops at a bar that no longer
|
/// half-state — an opaque bar over the board, or a board that stops at a bar that no longer
|
||||||
/// draws.
|
/// draws.
|
||||||
|
///
|
||||||
|
/// **The second knob does not stay written, and this is not the place that keeps it.** On a
|
||||||
|
/// SwiftUI scene window `titlebarAppearsTransparent` is SwiftUI's: it is the AppKit face of the
|
||||||
|
/// view tree's resolved `toolbarBackgroundVisibility(for: .windowToolbar)`, and SwiftUI writes
|
||||||
|
/// the resolved value — changed or not — on every pass in which it re-applies a window's
|
||||||
|
/// configuration. A tree that states nothing resolves `.automatic`, so each of those passes put
|
||||||
|
/// `false` back over what this method had set, and the board's title bar went opaque some time
|
||||||
|
/// after the board opened rather than at once. So the board *also* says it in SwiftUI
|
||||||
|
/// (`BoardWindowHost`, which owns the reasoning), and this write is what makes the posture true
|
||||||
|
/// for the turn in which the board's `background:` first reads, not what makes it last.
|
||||||
|
///
|
||||||
|
/// The `fullSizeContentView` half has no such second owner and survives on its own.
|
||||||
private func applyTitlebarExtensionIfPossible() {
|
private func applyTitlebarExtensionIfPossible() {
|
||||||
guard let window, let extendsUnderTitlebar else { return }
|
guard let window, let extendsUnderTitlebar else { return }
|
||||||
window.titlebarAppearsTransparent = extendsUnderTitlebar
|
window.titlebarAppearsTransparent = extendsUnderTitlebar
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import AppKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
import Testing
|
import Testing
|
||||||
@testable import Kanban
|
@testable import Kanban
|
||||||
|
|
||||||
@@ -441,3 +443,90 @@ struct BackgroundImagePathTests {
|
|||||||
#expect(BoardBackdrop.isCustom(try board("background: {color: mauve, image: /tmp/x.jpg}"), root: root) == false)
|
#expect(BoardBackdrop.isCustom(try board("background: {color: mauve, image: /tmp/x.jpg}"), root: root) == false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - The window chrome that follows the background
|
||||||
|
|
||||||
|
/// **A board that paints a background runs its content under a clear title bar** (03-board-ui.md §
|
||||||
|
/// Styling ▸ Capabilities), and that posture is said in two places on purpose.
|
||||||
|
///
|
||||||
|
/// `NSWindow.titlebarAppearsTransparent` belongs to SwiftUI on a scene window — it is the AppKit
|
||||||
|
/// face of `toolbarBackgroundVisibility(for: .windowToolbar)`, and SwiftUI writes the value its tree
|
||||||
|
/// resolves on every pass in which it re-applies a window's configuration. A tree that said nothing
|
||||||
|
/// resolved `.automatic`, so those passes wrote `false` back over the flag `HostedWindowController`
|
||||||
|
/// had set out of band: the board opened correct and its title bar turned opaque some time later,
|
||||||
|
/// painting its own material and its separator hairline across a picture still being drawn full
|
||||||
|
/// height underneath. So `BoardWindowHost` states it in SwiftUI as well, and the two tests here pin
|
||||||
|
/// the two halves — the rule SwiftUI is told, and the window the controller writes.
|
||||||
|
@MainActor
|
||||||
|
@Suite("Board background ▸ the window chrome follows it")
|
||||||
|
struct BoardChromeTests {
|
||||||
|
|
||||||
|
@Test("The toolbar background is gone over a board that paints one, and the system's otherwise")
|
||||||
|
func toolbarBackgroundFollowsTheBoard() {
|
||||||
|
#expect(BoardWindowHost.toolbarBackground(overCustomBackground: true) == .hidden)
|
||||||
|
// `.automatic`, never `.visible` — a board with no background of its own keeps whatever the
|
||||||
|
// system would have drawn, which is what "the standard chrome, untouched" means.
|
||||||
|
#expect(BoardWindowHost.toolbarBackground(overCustomBackground: false) == .automatic)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("The controller says nothing about a window's title bar until it is told")
|
||||||
|
func silentUntilTold() {
|
||||||
|
let window = Self.window()
|
||||||
|
let controller = HostedWindowController()
|
||||||
|
controller.attach(to: window)
|
||||||
|
|
||||||
|
// `nil` and `false` render identically; they differ only in whether this controller has
|
||||||
|
// spoken, which is what keeps the loading half from stating a default it does not own.
|
||||||
|
#expect(window.titlebarAppearsTransparent == false)
|
||||||
|
#expect(window.styleMask.contains(.fullSizeContentView) == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Both knobs move together, in both directions, however often they are set")
|
||||||
|
func bothKnobsMoveTogether() {
|
||||||
|
let window = Self.window()
|
||||||
|
let controller = HostedWindowController()
|
||||||
|
controller.attach(to: window)
|
||||||
|
|
||||||
|
controller.setExtendsContentUnderTitlebar(true)
|
||||||
|
#expect(window.titlebarAppearsTransparent)
|
||||||
|
#expect(window.styleMask.contains(.fullSizeContentView))
|
||||||
|
|
||||||
|
// Repeat-safe: the board drives this off its snapshot, so it is called on every reload that
|
||||||
|
// changes the reading and on plenty that do not.
|
||||||
|
controller.setExtendsContentUnderTitlebar(true)
|
||||||
|
#expect(window.titlebarAppearsTransparent)
|
||||||
|
#expect(window.styleMask.contains(.fullSizeContentView))
|
||||||
|
|
||||||
|
// And back — a `background:` deleted on disk puts the standard chrome back.
|
||||||
|
controller.setExtendsContentUnderTitlebar(false)
|
||||||
|
#expect(window.titlebarAppearsTransparent == false)
|
||||||
|
#expect(window.styleMask.contains(.fullSizeContentView) == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("The posture survives the provisional-window swap")
|
||||||
|
func theSlotReappliesToTheNextWindow() {
|
||||||
|
// SwiftUI dismantles and re-makes the background representable while it moves a scene's
|
||||||
|
// content into its final window, so a posture stated against the first window has to reach
|
||||||
|
// the second (`HostedWindowController.detach`).
|
||||||
|
let provisional = Self.window()
|
||||||
|
let controller = HostedWindowController()
|
||||||
|
controller.attach(to: provisional)
|
||||||
|
controller.setExtendsContentUnderTitlebar(true)
|
||||||
|
|
||||||
|
controller.detach()
|
||||||
|
let real = Self.window()
|
||||||
|
controller.attach(to: real)
|
||||||
|
|
||||||
|
#expect(real.titlebarAppearsTransparent)
|
||||||
|
#expect(real.styleMask.contains(.fullSizeContentView))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func window() -> NSWindow {
|
||||||
|
NSWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: 600, height: 400),
|
||||||
|
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||||
|
backing: .buffered,
|
||||||
|
defer: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user