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:
@@ -106,6 +106,11 @@ struct BoardView: View {
|
||||
/// Increase Contrast, for the marquee band's border below (10-accessibility.md; `Accommodations`).
|
||||
@Environment(\.colorSchemeContrast) private var contrast
|
||||
|
||||
/// Reduce Transparency, for the backdrop's title-bar frost — the one glass underlay this view
|
||||
/// draws (10-accessibility.md: "glass underlays go solid, wherever they appear";
|
||||
/// `Accommodations.frost`).
|
||||
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
|
||||
|
||||
/// Whether the strip holds keyboard focus, which is what makes the grammar keys arrive. Restored
|
||||
/// deliberately whenever an inline editor closes: the field that had focus is gone, and Return
|
||||
/// must go back to meaning create/rename rather than nothing at all.
|
||||
@@ -418,13 +423,42 @@ struct BoardView: View {
|
||||
/// is what the design asks for — and it is why the board is the level 10-accessibility.md binds
|
||||
/// its ≥ 4.5:1 rule to: text does sit on it.
|
||||
///
|
||||
/// ### Three layers, and the window is the frame
|
||||
///
|
||||
/// The colour is the underlay, the image draws over it, and a frosted strip sits on top of both
|
||||
/// under the title bar. All of it runs the **full height of the window** — `ignoresSafeArea`
|
||||
/// here is what the window's own `fullSizeContentView` flip is for (`HostedWindowController
|
||||
/// .setExtendsContentUnderTitlebar`, driven from `BoardWindowHost`), and the two only ever move
|
||||
/// together: a board with no background of its own draws none of this and keeps the standard
|
||||
/// chrome exactly as it has always looked.
|
||||
///
|
||||
/// The **colour is painted even while an image is loading**, and stays painted underneath it: a
|
||||
/// decode is asynchronous (`BoardBackdropImage`) and a window that flashed the system background
|
||||
/// on open would be the hitch that work exists to avoid. It is also what a failed or missing
|
||||
/// image degrades to, with nothing said about it.
|
||||
///
|
||||
/// The **frost** is the price of the extended chrome: the title bar's own material is gone, so
|
||||
/// the traffic lights, the board-name widget and the toolbar would otherwise sit directly on a
|
||||
/// saturated colour or a photograph. It is a glass underlay (`Accommodations.frost` — the
|
||||
/// ladder's thin weight, both ends tried and retired: `.bar` reads as barely-there over a
|
||||
/// busy image, `.regular` and up as a veil the backdrop shouldn't pay for) and takes the
|
||||
/// standard accommodation: solid
|
||||
/// under Reduce Transparency, "wherever they appear". Its geometry is `frostStrip`'s — full
|
||||
/// strength through the top safe-area inset, dissolving over a short tail below it — with the
|
||||
/// inset read from a `GeometryReader` that is itself inside the `ignoresSafeArea`: the proxy
|
||||
/// still reports the inset it was told to ignore, which is exactly the title-bar-plus-toolbar
|
||||
/// band and moves on its own when the toolbar's size class changes. Nothing here hit-tests, so
|
||||
/// the widget and the toolbar above it are untouched.
|
||||
///
|
||||
/// ### The contrast rule is the colour's, and the image is outside it
|
||||
///
|
||||
/// The rule is enforced from the *text* side rather than here, because this view paints the
|
||||
/// surface and draws none of the glyphs on it. Whatever colour lands below — a palette name or a
|
||||
/// hand-written hex, they reach the same place — has its text colour computed against the
|
||||
/// threshold by `BoardTextInk`, composited over the window background in the active appearance
|
||||
/// and recomputed on an appearance flip; the two subtrees that sit on this fill
|
||||
/// (`LaneView.header` and `TrashLaneView.header` — their plates are translucent washes the
|
||||
/// colour shows through, where every card carries its own opaque plate,
|
||||
/// surface and draws none of the glyphs on it. Whatever colour lands below — a palette name, a
|
||||
/// hand-written hex, or the `color` subkey of the mapping form, they reach the same place — has
|
||||
/// its text colour computed against the threshold by `BoardTextInk`, composited over the window
|
||||
/// background in the active appearance and recomputed on an appearance flip; the two subtrees
|
||||
/// that sit on this fill (`LaneView.header` and `TrashLaneView.header` — their plates are
|
||||
/// translucent washes the colour shows through, where every card carries its own opaque plate,
|
||||
/// `BoardSurface.cardPlate`) take the answer as a `\.colorScheme` override.
|
||||
///
|
||||
/// **One path, two verification stories** (`ContrastMath`): the twelve palette pairs are checked
|
||||
@@ -433,12 +467,69 @@ struct BoardView: View {
|
||||
/// cannot be settled by a table of colours alone); an arbitrary hex is checked only as it
|
||||
/// renders, because its value arrives from a file.
|
||||
///
|
||||
/// **An image makes no AA claim at all**, and the ink does not try to derive one from it. Ink
|
||||
/// still follows the `color` reading — the colour the author chose to sit under the picture, or
|
||||
/// the default when they chose none — which is the same bytes-from-a-file posture an arbitrary
|
||||
/// hex already has, one step further out: a photograph has no single luminance to threshold
|
||||
/// against, the field has no in-app control that could warn about one, and a per-pixel answer
|
||||
/// would change as the window resized. An author who lays text over a busy picture is doing what
|
||||
/// the raw file exists to let them do.
|
||||
///
|
||||
/// A value that resolves to nothing paints nothing, so the window keeps the standard background:
|
||||
/// the same lenient degrade as the other two levels, and the bytes stay as written.
|
||||
@ViewBuilder
|
||||
private var boardBackground: some View {
|
||||
if let color = Palette.color(for: store.snapshot.background) {
|
||||
color
|
||||
let color = Palette.color(for: store.snapshot.background)
|
||||
let image = BoardBackdrop.imageURL(for: store.snapshot, root: store.rootURL)
|
||||
if color != nil || image != nil {
|
||||
GeometryReader { proxy in
|
||||
ZStack(alignment: .top) {
|
||||
color
|
||||
if let image {
|
||||
BoardBackdropImage(url: image, reloads: store.landedReloads)
|
||||
}
|
||||
frostStrip(inset: proxy.safeAreaInsets.top)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
// A background is scenery: the strip's own empty-surface gestures — the click that
|
||||
// clears the selection, the rubber band — live in `backdrop`, one layer in, and would be
|
||||
// swallowed by anything here that answered a hit test.
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// The frost, full-strength through the title-bar band and dissolving over a short tail below
|
||||
/// it — a scroll-edge dissolve rather than a shelf. The chrome sits on an even material the
|
||||
/// whole way down, and the strip's bottom edge is nowhere in particular, so the backdrop reads
|
||||
/// as one surface the chrome floats over rather than a bar laid across a picture. The tail is a
|
||||
/// fraction of the band, so it scales with the toolbar's own height and only ever reaches into
|
||||
/// the strip's outer padding, not the lanes.
|
||||
///
|
||||
/// Under Reduce Transparency the fade goes with the glass: "solid" means an honest opaque bar
|
||||
/// with the standard chrome's own hard edge (`Accommodations.frost`), not a solid that thins
|
||||
/// out — a partially transparent solid would be the setting's own defeat.
|
||||
@ViewBuilder
|
||||
private func frostStrip(inset: CGFloat) -> some View {
|
||||
let underlay = Accommodations.frost(reduceTransparency: reduceTransparency)
|
||||
if underlay == .solid {
|
||||
Rectangle().fill(underlay.style).frame(height: inset)
|
||||
} else if inset > 0 {
|
||||
let tail = inset * 0.35
|
||||
Rectangle()
|
||||
.fill(underlay.style)
|
||||
.frame(height: inset + tail)
|
||||
.mask {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .black, location: 0),
|
||||
.init(color: .black, location: inset / (inset + tail)),
|
||||
.init(color: .clear, location: 1),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user