import AppKit import SwiftUI /// The system's **visual accommodations**, as one named surface — `Motion`'s sibling /// (10-accessibility.md ▸ Text scaling & visual accommodations). Motion owns Reduce Motion; this owns /// the other two settings the design commits to, plus the one that decides whether a focus ring is /// furniture or a lifeline: /// /// - **Increase Contrast** — "strengthens borders and the selection indicator"; /// - **Reduce Transparency** — "glass underlays go solid, wherever they appear"; /// - **Full Keyboard Access** — "the board is one tab stop with arrow-key navigation within". /// /// The rule this type exists to enforce is `Motion`'s, transplanted: **no call site anywhere decides /// for itself what an accommodation means.** A view that draws a border asks for a border width by /// meaning and passes in what the environment reports; what "increased" does to that width is /// decided once, here, so a new bordered surface inherits the answer instead of inventing one. /// /// ### Why the decisions are pure functions of an environment value /// /// Same reason `Motion.reloadAnimates` is: so a test can hold them still. Nothing below renders, and /// the claims 10-accessibility.md actually makes — a heavier ring under Increase Contrast, a solid /// underlay under Reduce Transparency — are assertable only if the decision is separable from the /// drawing. The two `AnyShapeStyle`-producing families therefore go through small `Equatable` enums /// (`Underlay`, `Wash`) exactly as the transitions go through `Motion.Appearance`, because /// `AnyShapeStyle` is opaque and a claim about it would be untestable. /// /// ### Where the values come from /// /// Views read `@Environment(\.colorSchemeContrast)` and `@Environment(\.accessibilityReduceTransparency)` /// and pass them in. Code with no environment to read asks AppKit the same questions /// (`prefersIncreasedContrast`, `prefersReducedTransparency`) — `Motion.prefersReducedMotion`'s /// pattern, for its reason. enum Accommodations { // MARK: - Increase Contrast /// A stroke's width: `base` normally, **one point heavier** when the user has asked for stronger /// borders (10-accessibility.md: "Increase Contrast strengthens borders and the selection /// indicator"). /// /// One point rather than a multiplier, deliberately. The board's strokes span 1pt (a well's /// separator hairline) to 3pt (the template chooser's selection frame), and a factor that made /// the hairline legible would turn the chooser's frame into a slab. A flat point is what the /// system's own controls do under the setting, and it is monotone: a heavier stroke stays /// heavier than a lighter one, so the visual hierarchy the widths encode survives the setting. /// /// It is *not* scaled by the text size, and that is a ruling rather than an oversight: a border /// is a hairline against a background, not a glyph — AppKit's own controls keep their stroke /// weights across text sizes, and a 3pt selection ring at a large text size would read as a /// fill. static func borderWidth(_ base: CGFloat, contrast: ColorSchemeContrast) -> CGFloat { contrast == .increased ? base + 1 : base } /// Whether a plate that normally floats on its fill alone draws an **outline** at all. /// /// This is the other half of "strengthens borders", and the half that is easy to miss: a card /// face and a lane plate carry no resting border — they are a fill against the board background, /// which is exactly the distinction Increase Contrast exists to rescue for a user who cannot see /// it. So under the setting they gain a hairline in the separator colour, and the selection ring /// above stays what it always was: the *accent*-coloured one, still unambiguous against it. /// /// Chrome, never information: nothing about the board's meaning changes, so nothing has to be /// said differently to VoiceOver when this flips. static func drawsRestingBorder(contrast: ColorSchemeContrast) -> Bool { contrast == .increased } /// A decorative accent drawn at reduced alpha — the marquee band's border, the drag shadow's /// dashes, the new-card editor's well — taken to **full strength** under Increase Contrast. /// /// Alpha is the other way a border can be weak, and a width bump alone would leave a 55%-alpha /// dashed outline just as hard to see two points wider. static func accentOpacity(_ base: Double, contrast: ColorSchemeContrast) -> Double { contrast == .increased ? 1 : base } /// Increase Contrast, asked of AppKit rather than of the SwiftUI environment — for callers built /// outside a rendered hierarchy, where the environment's accessibility values are not reliably /// populated (`Motion.prefersReducedMotion`'s constituency). @MainActor static var prefersIncreasedContrast: Bool { NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast } // MARK: - Reduce Transparency /// What a **glass underlay** is made of — a real material, or the solid the setting replaces it /// with (10-accessibility.md: "Reduce Transparency: glass underlays go solid, wherever they /// appear"). /// /// The design's own example (the card face carousel's page dots) died with the carousel /// (03-board-ui.md § Card face's no-carousel resettlement), so the rule's one surviving subject /// on the board is the transient search bar's `.bar` material. It is stated as a type anyway /// rather than inlined at that one call site, because "wherever they appear" is a standing rule /// and the next material to arrive should find the answer already written. enum Underlay: Equatable { /// `Material.bar` — the find-bar's own backdrop, translucent over the board beneath it. case glass /// The window's own background colour, opaque. case solid var style: AnyShapeStyle { switch self { case .glass: AnyShapeStyle(.bar) case .solid: AnyShapeStyle(Color(nsColor: .windowBackgroundColor)) } } } static func underlay(reduceTransparency: Bool) -> Underlay { reduceTransparency ? .solid : .glass } /// A **translucent wash** — a tint laid over whatever happens to be behind it, and the shape /// every non-material translucency on the board takes: every lane's plate, the trash column's /// plate and hatched header, and the drag shadow's fill. /// /// These are not glass, and the distinction matters enough to keep two types: a material samples /// and blurs its backdrop, a wash simply composites at an alpha. But they fail the same way for /// the same user — the board's `background` is a colour the *user* chose (03-board-ui.md § /// Styling), so a 35%-alpha plate over a saturated board is exactly the "what is behind this" /// problem Reduce Transparency exists to remove. Under the setting each one takes the standard /// secondary background instead, which is opaque and appearance-aware. enum Wash: Equatable { /// `.quaternary` at `opacity`, over whatever is behind. case translucent(opacity: Double) /// The standard secondary background — opaque, so nothing shows through. case opaque var style: AnyShapeStyle { switch self { case let .translucent(opacity): AnyShapeStyle(HierarchicalShapeStyle.quaternary.opacity(opacity)) case .opaque: AnyShapeStyle(.background.secondary) } } } /// Every lane's resting plate (03-board-ui.md § Styling ▸ Capabilities — the standard chrome's /// middle step, the pathfinder's lane surface carried over). Translucent so a board-chosen /// colour shows through it, which is what keeps `BoardTextInk`'s premise true for the header /// text sitting on it. static func lanePlateWash(reduceTransparency: Bool) -> Wash { reduceTransparency ? .opaque : .translucent(opacity: 0.35) } /// The trash column's plate — the same figure as an ordinary lane's, kept as its own knob /// because the trash answers to its own rendering spec (03-board-ui.md § Trash ▸ Rendering), /// and the quietest of its treatments: the hatched header above carries the column's identity. static func trashPlateWash(reduceTransparency: Bool) -> Wash { reduceTransparency ? .opaque : .translucent(opacity: 0.35) } /// The trash column's hatched header. Heavier than the plate, because it is the whole of "you /// are looking at the trash" (03-board-ui.md § Trash ▸ Rendering). static func trashHeaderWash(reduceTransparency: Bool) -> Wash { reduceTransparency ? .opaque : .translucent(opacity: 0.5) } /// A drop shadow's fill — the outline occupying an item's proposed landing spot (`DragShadow`). static func dragShadowWash(reduceTransparency: Bool) -> Wash { reduceTransparency ? .opaque : .translucent(opacity: 0.5) } /// Reduce Transparency, asked of AppKit — `prefersIncreasedContrast`'s twin, same constituency. @MainActor static var prefersReducedTransparency: Bool { NSWorkspace.shared.accessibilityDisplayShouldReduceTransparency } // MARK: - Full Keyboard Access /// Whether the system's **Full Keyboard Access** is on. /// /// It has one caller and one purpose: the board strip suppresses its focus ring, because "the /// strip is the window's content, not a control, and a rectangle around the whole board would /// read as an error state" (`BoardView`) — and that reasoning inverts completely under FKA, /// where 10-accessibility.md makes the board **one tab stop** and a tab stop nobody can see is /// not one. So the ring comes back exactly when Tab can land on it. /// /// There is no SwiftUI environment value for this and no change notification to observe, so it /// is read at body evaluation like any other system query here. That is honest for what it is: a /// setting a user turns on once (⌃F7, or System Settings ▸ Keyboard), not one that flips during /// a gesture — and a board window re-renders on nearly every interaction, so a flip is picked up /// almost immediately rather than never. @MainActor static var isFullKeyboardAccessEnabled: Bool { NSApp?.isFullKeyboardAccessEnabled ?? false } }