The board wears a picture — background becomes a mapping, and the window chrome follows it under a thin frost
background is {color:, image:} and only a mapping at every level; the board's image paints the full window under a transparent title bar, with a thin-material frost strip keeping the chrome legible and the standard accommodations intact.
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -203,6 +203,21 @@ struct BoardWindowHost: View {
|
||||
.onChange(of: boardSearch.isFocused) { _, _ in
|
||||
boardSearch.dismissTransientIfCleared(query: store.searchQuery)
|
||||
}
|
||||
// **The window chrome follows the board's background** (03-board-ui.md § Styling ▸
|
||||
// Capabilities): a board that paints one runs its content the full height of the frame
|
||||
// under a transparent title bar, with `BoardView.boardBackground`'s frosted strip
|
||||
// keeping the widget and the toolbar legible over it; a board that paints none keeps
|
||||
// the standard chrome untouched.
|
||||
//
|
||||
// Here rather than in `configureWindow` because it is not a wiring fact but a *live*
|
||||
// one: `background` is hand-editable, the watcher reloads on a change to `index.md`, and
|
||||
// the chrome has to follow the reading in both directions. `initial: true` because the
|
||||
// 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
|
||||
// (`HostedWindowController.extendsUnderTitlebar`).
|
||||
.onChange(of: BoardBackdrop.isCustom(store.snapshot, root: store.rootURL), initial: true) { _, custom in
|
||||
windowController.setExtendsContentUnderTitlebar(custom)
|
||||
}
|
||||
// **The board settings sheet** (03-board-ui.md ▸ Board settings sheet) — presented from
|
||||
// the board window's own content, which is what makes it modal to *this* board rather
|
||||
// than to the app: "a board-scoped, titled, sectioned sheet on the board window".
|
||||
|
||||
@@ -109,6 +109,24 @@ final class HostedWindowController: NSObject, NSWindowDelegate {
|
||||
/// what the chrome draws from it.
|
||||
private var titleVisibility: NSWindow.TitleVisibility?
|
||||
|
||||
/// Whether this window's content runs the full height of the frame, under a transparent title
|
||||
/// bar — **a board window carrying a custom background**, and nothing else (03-board-ui.md §
|
||||
/// Styling ▸ Capabilities: the board's colour or image "paints the full window"; `BoardView
|
||||
/// .boardBackground` draws the frosted strip that keeps the chrome legible over it).
|
||||
///
|
||||
/// `nil` leaves AppKit's own posture untouched, exactly as `titleVisibility` does — the welcome,
|
||||
/// bootstrap and card windows have no opinion, and neither does a board window while it loads
|
||||
/// (the flag is driven off the snapshot, which does not exist yet). `nil` and `false` therefore
|
||||
/// render identically; they differ only in whether this controller has *said* anything, which is
|
||||
/// what keeps the loading half from having to state a default it does not own.
|
||||
///
|
||||
/// A slot rather than a one-shot write, and **repeat-safe rather than install-once** — the
|
||||
/// `hideTitle` pattern, for a stronger version of its reason: the value has to survive the
|
||||
/// provisional-window swap (`detach()`), *and* it genuinely changes over a window's life. A
|
||||
/// `background:` edited on disk reloads the snapshot, and the chrome follows it in both
|
||||
/// directions.
|
||||
private var extendsUnderTitlebar: Bool?
|
||||
|
||||
private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "window")
|
||||
|
||||
// MARK: Attachment
|
||||
@@ -129,6 +147,7 @@ final class HostedWindowController: NSObject, NSWindowDelegate {
|
||||
addTitlebarAccessoryIfPossible()
|
||||
applyToolbarIfPossible()
|
||||
applyTitleVisibilityIfPossible()
|
||||
applyTitlebarExtensionIfPossible()
|
||||
}
|
||||
|
||||
/// Puts the previous delegate back and takes the titlebar accessory and toolbar off the window —
|
||||
@@ -233,6 +252,39 @@ final class HostedWindowController: NSObject, NSWindowDelegate {
|
||||
window.titleVisibility = titleVisibility
|
||||
}
|
||||
|
||||
// MARK: Content under the title bar
|
||||
|
||||
/// Runs this window's content the full height of its frame, under a transparent title bar — or
|
||||
/// puts the standard chrome back (see `extendsUnderTitlebar`).
|
||||
///
|
||||
/// Safe whenever the caller learns the answer — before the window exists (held, applied at
|
||||
/// `attach`) or after (applied now) — and safe to call repeatedly with the same value, which
|
||||
/// matters more here than for `hideTitle`: the board window drives this off its snapshot, so it
|
||||
/// is called on every reload that changes the reading and on plenty that do not.
|
||||
///
|
||||
/// **Not undone at `detach`**, `titleVisibility`'s posture: the slot survives the provisional-
|
||||
/// window swap and reapplies itself to whichever window attaches next, and a window that is
|
||||
/// genuinely going away takes its chrome with it.
|
||||
func setExtendsContentUnderTitlebar(_ flag: Bool) {
|
||||
extendsUnderTitlebar = flag
|
||||
applyTitlebarExtensionIfPossible()
|
||||
}
|
||||
|
||||
/// The two AppKit knobs the effect needs, and they are one decision: `fullSizeContentView` is
|
||||
/// what lets the content view reach under the title bar, and `titlebarAppearsTransparent` is
|
||||
/// 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
|
||||
/// draws.
|
||||
private func applyTitlebarExtensionIfPossible() {
|
||||
guard let window, let extendsUnderTitlebar else { return }
|
||||
window.titlebarAppearsTransparent = extendsUnderTitlebar
|
||||
if extendsUnderTitlebar {
|
||||
window.styleMask.insert(.fullSizeContentView)
|
||||
} else {
|
||||
window.styleMask.remove(.fullSizeContentView)
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the window for real, after the flush has run. `performClose` rather than `close` so the
|
||||
/// standard path runs — SwiftUI's own delegate gets its callbacks, tabbing behaves — with the
|
||||
/// flag telling our own `windowShouldClose` to stand aside.
|
||||
|
||||
Reference in New Issue
Block a user