Files
punktfunk/clients/apple/Sources/PunktfunkClient/Home/GamepadInk.swift
T
enricobuehler c010139e6e
ci / bun-nix (pull_request) Successful in 49s
ci / web (pull_request) Successful in 1m3s
ci / docs-site (pull_request) Successful in 1m16s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 4m38s
ci / rust (pull_request) Successful in 16m9s
feat(client/apple): the gamepad UI moves and colours like the console it mirrors
Four reworks from the first palette-era on-glass review, all iOS-facing:

- Surfaces carry the palette now, not just the text on them: ConsoleGlass
  washes every tier (Liquid Glass tint, pre-26 material, tvOS material)
  with ink.glass — the same colour the desktop console fills its panels
  with — and the close buttons move to an ink-aware consoleGlassBackground.
  The pre-26 branch also gains the focus tint it had silently dropped.
  Stray literals follow: ConnectOverlay text rides ink in the console
  takeover, card shadows soften on pale fields, the focused keycap reads
  onAccent. The online pip stays status-green on purpose.

- The header breathes: title top padding 4/10 -> 10/18 plus shared
  header-spacing and title-bottom helpers mapped from the console shell's
  rhythm, applied to the launcher, settings and add-host alike, with the
  add-host close X re-anchored to the title row.

- Settings, Add Host and the Library present IN PLACE on iOS: one
  persistent aurora whose calm is chased (the console's bg_mix), screens
  as transparent layers with the console's 0.26 s ease-out-cubic push/pop,
  an input drop for the transition, and the controller handed off through
  isActive — no more opaque bottom-up covers, no backdrop teardown.
  macOS keeps its sheets, tvOS its focus-engine covers.

- The settings select is a real band: choice rows mount GamepadOptionBand,
  a spring-driven drum (Animatable body, ring-distance wrap, neighbours
  gated by focus and flight) whose retargeting spring accumulates rapid
  steps into one continuous spin. Reduce Motion falls back to a plain
  crossfade; toggles keep the quiet 14 pt slip.

Verified: swift build (macOS), swift build --triple arm64-apple-ios17.0,
swift test 208 passed / 0 failed. On-glass QA still owed: palette sweep on
a pale palette, transition compositing over materials, drum feel on device.
2026-08-07 13:59:40 +02:00

102 lines
4.7 KiB
Swift

// The ink the gamepad UI draws with under the chosen background palette.
//
// The console screens were white-on-dark throughout with the brand violet hardcoded as the
// accent. Both had to become palette-derived at once: a pale field needs dark text or it is
// unreadable, and a violet focus wash on a copper field is exactly the clash this exists to fix.
//
// Handed down the view tree as an environment value rather than passed to each screen, so a
// leaf (a row, a hint pill, a card) can ask for the right colour without every caller in between
// knowing about palettes. `pf-console-ui` does the same thing with a thread-local `Ink`.
import PunktfunkShared
import SwiftUI
#if os(iOS) || os(macOS) || os(tvOS)
struct GamepadInk: Equatable, Sendable {
/// Primary text/glyph colour.
let fg: Color
/// Focus wash, selected tab pill, switch track, caret — the palette's own accent.
let accent: Color
/// What reads ON the accent (a filled pill's label).
let onAccent: Color
/// The base fill every glass surface starts from.
let glass: Color
/// What a wash laid UNDER text tends toward: black on a dark field, white on a pale one.
let shade: Color
/// How hard those washes go. A pale field needs far less — mixing toward white at the dark
/// field's strength bleaches the chroma straight out of the gradient.
let shadeScale: Double
/// True when the field is pale, for the few places that need to branch rather than blend
/// (a material's `colorScheme`, a shadow's presence).
let isLight: Bool
/// The foreground at `alpha`.
func fg(_ alpha: Double) -> Color { fg.opacity(alpha) }
/// The accent at `alpha`.
func accent(_ alpha: Double) -> Color { accent.opacity(alpha) }
/// A wash under text: `alpha` is the dark-field strength, scaled for a pale one.
func shade(_ alpha: Double) -> Color { shade.opacity(alpha * shadeScale) }
/// The glass base at `alpha` — what a surface's material is washed with so it carries the
/// palette's hue (the console fills its panels with exactly this colour).
func glass(_ alpha: Double) -> Color { glass.opacity(alpha) }
/// A drop shadow: always black — a white shadow is not a shadow — but softened on a pale
/// field, where full-strength black under every card reads as a smear rather than depth.
func shadow(_ alpha: Double) -> Color { .black.opacity(alpha * (isLight ? 0.4 : 1)) }
static func of(_ p: GamepadPalette) -> GamepadInk {
let accent = Color(red: p.accent.x, green: p.accent.y, blue: p.accent.z)
let accentLuma = 0.2126 * p.accent.x + 0.7152 * p.accent.y + 0.0722 * p.accent.z
// Chosen by luminance, not by `light`: an accent is picked for contrast against the
// GLASS, not against the field.
let onAccent: Color = accentLuma > 0.55 ? .black : .white
guard p.light else {
return GamepadInk(
fg: .white, accent: accent, onAccent: onAccent,
glass: Color(red: 0.086, green: 0.086, blue: 0.125),
shade: .black, shadeScale: 1, isLight: false)
}
return GamepadInk(
// Tinted toward the palette's own ground so it doesn't read as a foreign grey.
fg: Color(red: p.ground.x * 0.16, green: p.ground.y * 0.14, blue: p.ground.z * 0.20),
accent: accent, onAccent: onAccent,
glass: .white,
shade: .white, shadeScale: 0.45, isLight: true)
}
/// The shipped dark look — what a preview or a test composition gets.
static let dark = GamepadInk.of(GamepadPalette.named("violet"))
/// The online pip — deliberately NOT palette-derived: a status colour must not change
/// meaning with the wallpaper (the console's rule; this is its `ONLINE_GREEN` verbatim).
static let onlineGreen = Color(red: 0.20, green: 0.84, blue: 0.29)
}
private struct GamepadInkKey: EnvironmentKey {
static let defaultValue = GamepadInk.dark
}
extension EnvironmentValues {
/// The ink of the palette currently drawing. Set once, high up (see `GamepadInkModifier`).
var gamepadInk: GamepadInk {
get { self[GamepadInkKey.self] }
set { self[GamepadInkKey.self] = newValue }
}
}
extension View {
/// Resolve the stored `ui_palette` and publish its ink to everything below. Applied by the
/// gamepad screens' common root so no individual view has to read the setting.
func gamepadPaletteInk() -> some View { modifier(GamepadInkModifier()) }
}
private struct GamepadInkModifier: ViewModifier {
@AppStorage(DefaultsKey.uiPalette) private var paletteID = "violet"
func body(content: Content) -> some View {
content.environment(\.gamepadInk, GamepadInk.of(GamepadPalette.named(paletteID)))
}
}
#endif