From a14b000c9bacc9cede18382b2af235defedb09d2 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 10 Aug 2026 10:11:18 +0200 Subject: [PATCH] =?UTF-8?q?feat(apple):=20the=20tray=20gradient=20blur=20i?= =?UTF-8?q?s=20back=20=E2=80=94=20as=20a=20real=20blur=20this=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The form screens' pinned title and legend lost their gradient blur when GamepadTrayScrim went (2590238b): the scrim was `.ultraThinMaterial`, and a material by definition lifts and tints whatever it blurs — it read grey over the aurora, and washed with the palette's ground it read coloured. There is no public way to blur a backdrop without that tint, so the layer was deleted rather than re-tuned, and the rows have scrolled to a raw edge since. Glur's GlurView blurs the backdrop through a gradient with NO material stage on top — the rows soften as they slide under the pinned trays and nothing carries a colour. Three things about the adoption are deliberate: - It is the library's GlurBackdrop product, which uses a private API (a variable-blur CAFilter on UIKit/AppKit). The public `.glur()` modifier is a Metal shader on a view's OWN content and silently no-ops over platform-backed views like ScrollView, so it cannot reach a backdrop at all. GlurBackdrop ships as a separate module precisely so adopters can weigh the App Store exposure; it disables hit testing itself, so the band never eats a touch. - The dependency is pinned by REVISION: GlurBackdrop exists only on main — no release carries it (the newest tag, `1.1`, predates it and is not three-component semver anyway, so `from:` resolution stops at 1.0.4, which doesn't have the product). Wired into BOTH manifests — the package for `swift build`, and the xcodeproj (remote package reference + per-target product deps) for the app targets, which compile these sources themselves and know nothing of Package.swift's dependencies. - The band takes NO content-side overhang, unlike the scrim it replaces — and dropping the scrim's hard-won -44/-72 runway is the point, not an oversight. That runway existed because a material carries body at every alpha and had to dissolve OUTSIDE the tray; carried over here it blurred fully-visible rows at rest (first on-glass verdict). Glur's ramp reaches literal zero, so the band ends exactly at the tray's layout edge: open content untouched, rows soften only where they actually slide under. The 80 pt outer bleed and the pinned shape (`transaction { $0.animation = nil }`) are kept — full-bleed by LAYOUT rather than `.ignoresSafeArea()`, so a push never shows the band growing into place. Mounted exactly where the scrim was: settings, add-host and pair trays (top + bottom) and the library title (top). Glur is attributed on the Acknowledgements screen alongside the bundled font — it is a Swift package, so the Rust-closure notices generator rightly knows nothing about it. macOS build, tvOS typecheck; band placement and the blur itself verified on the iPad on glass. --- clients/apple/Package.resolved | 13 +++++ clients/apple/Package.swift | 18 +++++- .../apple/Punktfunk.xcodeproj/project.pbxproj | 57 +++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 10 +++- .../Home/GamepadAddHostView.swift | 2 + .../PunktfunkClient/Home/GamepadChrome.swift | 41 +++++++++++++ .../Home/GamepadLibraryScreen.swift | 1 + .../Settings/AcknowledgementsView.swift | 9 +++ .../Settings/GamepadSettingsView.swift | 2 + .../Trust/GamepadPairView.swift | 2 + 10 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 clients/apple/Package.resolved diff --git a/clients/apple/Package.resolved b/clients/apple/Package.resolved new file mode 100644 index 00000000..b792f51b --- /dev/null +++ b/clients/apple/Package.resolved @@ -0,0 +1,13 @@ +{ + "pins" : [ + { + "identity" : "glur", + "kind" : "remoteSourceControl", + "location" : "https://github.com/joogps/Glur.git", + "state" : { + "revision" : "ba4f05d3c9a608ec773b9305f2af6089390de68a" + } + } + ], + "version" : 2 +} diff --git a/clients/apple/Package.swift b/clients/apple/Package.swift index 9379f13b..0464dd60 100644 --- a/clients/apple/Package.swift +++ b/clients/apple/Package.swift @@ -16,6 +16,17 @@ let package = Package( .library(name: "PunktfunkShared", targets: ["PunktfunkShared"]), .executable(name: "PunktfunkClient", targets: ["PunktfunkClient"]), ], + dependencies: [ + // Progressive (gradient) backdrop blur for the form screens' trays — a real blur with no + // material tint stage (see GamepadTrayBlur). Pinned by REVISION, not `from:`: the + // GlurBackdrop product exists only on main — no release carries it (the newest tag, + // `1.1`, predates it, and is not three-component semver anyway, so version-based + // resolution stops at 1.0.4). The revision is main's head at adoption time; a revision + // pin stays reproducible when the branch moves. + .package( + url: "https://github.com/joogps/Glur.git", + revision: "ba4f05d3c9a608ec773b9305f2af6089390de68a"), + ], targets: [ .binaryTarget(name: "PunktfunkCore", path: "PunktfunkCore.xcframework"), // No dependencies by design — an extension process links this alone. @@ -51,7 +62,12 @@ let package = Package( // (The tvOS slide-transition package is referenced by the Xcode PROJECT only — // its manifest breaks SwiftPM whole-graph validation on macOS, and only the // Punktfunk-tvOS target links it; the #if os(tvOS) import never compiles here.) - .executableTarget(name: "PunktfunkClient", dependencies: ["PunktfunkKit"]), + .executableTarget( + name: "PunktfunkClient", + dependencies: [ + "PunktfunkKit", + .product(name: "GlurBackdrop", package: "Glur"), + ]), // PunktfunkCore is a direct dep too so the wire tests can name the C ABI's // `PunktfunkInputEvent` / `PUNKTFUNK_INPUT_KIND_*` when asserting the gamepad byte layout. .testTarget( diff --git a/clients/apple/Punktfunk.xcodeproj/project.pbxproj b/clients/apple/Punktfunk.xcodeproj/project.pbxproj index 92de9fea..bd0b5f5a 100644 --- a/clients/apple/Punktfunk.xcodeproj/project.pbxproj +++ b/clients/apple/Punktfunk.xcodeproj/project.pbxproj @@ -11,6 +11,12 @@ BB0000000000000000000005 /* PunktfunkKit in Frameworks */ = {isa = PBXBuildFile; productRef = BB0000000000000000000006 /* PunktfunkKit */; }; CC0000000000000000000005 /* PunktfunkKit in Frameworks */ = {isa = PBXBuildFile; productRef = CC0000000000000000000006 /* PunktfunkKit */; }; DD0000000000000000000003 /* SwiftUINavigationTransitions in Frameworks */ = {isa = PBXBuildFile; productRef = DD0000000000000000000002 /* SwiftUINavigationTransitions */; }; + EE0000000000000000000012 /* Glur in Frameworks */ = {isa = PBXBuildFile; productRef = EE0000000000000000000002 /* Glur */; }; + EE0000000000000000000013 /* GlurBackdrop in Frameworks */ = {isa = PBXBuildFile; productRef = EE0000000000000000000003 /* GlurBackdrop */; }; + EE0000000000000000000014 /* Glur in Frameworks */ = {isa = PBXBuildFile; productRef = EE0000000000000000000004 /* Glur */; }; + EE0000000000000000000015 /* GlurBackdrop in Frameworks */ = {isa = PBXBuildFile; productRef = EE0000000000000000000005 /* GlurBackdrop */; }; + EE0000000000000000000016 /* Glur in Frameworks */ = {isa = PBXBuildFile; productRef = EE0000000000000000000006 /* Glur */; }; + EE0000000000000000000017 /* GlurBackdrop in Frameworks */ = {isa = PBXBuildFile; productRef = EE0000000000000000000007 /* GlurBackdrop */; }; E295569A300948B9009F939C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2955699300948B9009F939C /* WidgetKit.framework */; }; E295569C300948B9009F939C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E295569B300948B9009F939C /* SwiftUI.framework */; }; E29556A9300948BA009F939C /* PunktfunkWidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E2955697300948B9009F939C /* PunktfunkWidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -88,6 +94,8 @@ buildActionMask = 2147483647; files = ( AA0000000000000000000005 /* PunktfunkKit in Frameworks */, + EE0000000000000000000012 /* Glur in Frameworks */, + EE0000000000000000000013 /* GlurBackdrop in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -96,6 +104,8 @@ buildActionMask = 2147483647; files = ( BB0000000000000000000005 /* PunktfunkKit in Frameworks */, + EE0000000000000000000014 /* Glur in Frameworks */, + EE0000000000000000000015 /* GlurBackdrop in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -105,6 +115,8 @@ files = ( CC0000000000000000000005 /* PunktfunkKit in Frameworks */, DD0000000000000000000003 /* SwiftUINavigationTransitions in Frameworks */, + EE0000000000000000000016 /* Glur in Frameworks */, + EE0000000000000000000017 /* GlurBackdrop in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -175,6 +187,8 @@ name = Punktfunk; packageProductDependencies = ( AA0000000000000000000006 /* PunktfunkKit */, + EE0000000000000000000002 /* Glur */, + EE0000000000000000000003 /* GlurBackdrop */, ); productName = Punktfunk; productReference = AA0000000000000000000001 /* Punktfunk.app */; @@ -201,6 +215,8 @@ name = "Punktfunk-iOS"; packageProductDependencies = ( BB0000000000000000000006 /* PunktfunkKit */, + EE0000000000000000000004 /* Glur */, + EE0000000000000000000005 /* GlurBackdrop */, ); productName = "Punktfunk-iOS"; productReference = BB0000000000000000000001 /* Punktfunk-iOS.app */; @@ -226,6 +242,8 @@ packageProductDependencies = ( CC0000000000000000000006 /* PunktfunkKit */, DD0000000000000000000002 /* SwiftUINavigationTransitions */, + EE0000000000000000000006 /* Glur */, + EE0000000000000000000007 /* GlurBackdrop */, ); productName = "Punktfunk-tvOS"; productReference = CC0000000000000000000001 /* Punktfunk-tvOS.app */; @@ -283,6 +301,7 @@ packageReferences = ( AA000000000000000000000F /* XCLocalSwiftPackageReference "." */, DD0000000000000000000001 /* XCRemoteSwiftPackageReference "swiftui-navigation-transitions" */, + EE0000000000000000000001 /* XCRemoteSwiftPackageReference "Glur" */, ); preferredProjectObjectVersion = 77; productRefGroup = AA0000000000000000000008 /* Products */; @@ -848,6 +867,14 @@ minimumVersion = 0.18.0; }; }; + EE0000000000000000000001 /* XCRemoteSwiftPackageReference "Glur" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/joogps/Glur.git"; + requirement = { + kind = revision; + revision = ba4f05d3c9a608ec773b9305f2af6089390de68a; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -868,6 +895,36 @@ package = DD0000000000000000000001 /* XCRemoteSwiftPackageReference "swiftui-navigation-transitions" */; productName = SwiftUINavigationTransitions; }; + EE0000000000000000000002 /* Glur */ = { + isa = XCSwiftPackageProductDependency; + package = EE0000000000000000000001 /* XCRemoteSwiftPackageReference "Glur" */; + productName = Glur; + }; + EE0000000000000000000003 /* GlurBackdrop */ = { + isa = XCSwiftPackageProductDependency; + package = EE0000000000000000000001 /* XCRemoteSwiftPackageReference "Glur" */; + productName = GlurBackdrop; + }; + EE0000000000000000000004 /* Glur */ = { + isa = XCSwiftPackageProductDependency; + package = EE0000000000000000000001 /* XCRemoteSwiftPackageReference "Glur" */; + productName = Glur; + }; + EE0000000000000000000005 /* GlurBackdrop */ = { + isa = XCSwiftPackageProductDependency; + package = EE0000000000000000000001 /* XCRemoteSwiftPackageReference "Glur" */; + productName = GlurBackdrop; + }; + EE0000000000000000000006 /* Glur */ = { + isa = XCSwiftPackageProductDependency; + package = EE0000000000000000000001 /* XCRemoteSwiftPackageReference "Glur" */; + productName = Glur; + }; + EE0000000000000000000007 /* GlurBackdrop */ = { + isa = XCSwiftPackageProductDependency; + package = EE0000000000000000000001 /* XCRemoteSwiftPackageReference "Glur" */; + productName = GlurBackdrop; + }; E2CAFE000000000000000002 /* PunktfunkShared */ = { isa = XCSwiftPackageProductDependency; productName = PunktfunkShared; diff --git a/clients/apple/Punktfunk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/clients/apple/Punktfunk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index be619c58..b50bbe78 100644 --- a/clients/apple/Punktfunk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/clients/apple/Punktfunk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,14 @@ { - "originHash" : "5d17a752eb57d190a90cbd663718ff44034b24fe0ae1baafea7677db2d49da6f", + "originHash" : "bb1ce9bc6042f166bd0aad78a15081e673781d3a90fa52fd8ec8a08875878ef6", "pins" : [ + { + "identity" : "glur", + "kind" : "remoteSourceControl", + "location" : "https://github.com/joogps/Glur.git", + "state" : { + "revision" : "ba4f05d3c9a608ec773b9305f2af6089390de68a" + } + }, { "identity" : "objc-runtime-tools", "kind" : "remoteSourceControl", diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadAddHostView.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadAddHostView.swift index 751c4f31..fd032eec 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadAddHostView.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadAddHostView.swift @@ -73,6 +73,7 @@ struct GamepadAddHostView: View { .padding(.top, gamepadTitleTopPadding(compact: compact)) .padding(.bottom, gamepadTitleBottomPadding(compact: compact)) .frame(maxWidth: .infinity, alignment: .leading) + .background { GamepadTrayBlur(edge: .top) } } .safeAreaInset(edge: .bottom, spacing: 0) { bottomTray @@ -83,6 +84,7 @@ struct GamepadAddHostView: View { gamepadLegendBottomPadding( compact ? 12 : 18, tier: metrics.tier, displayBottom: displayBottomInset)) .padding(.top, compact ? 6 : 10) + .background { GamepadTrayBlur(edge: .bottom) } } // No aurora — the same clean Liquid-Glass-over-dark base as the gamepad settings screen. // Hosted in the shell, the field is the shell's (see GamepadSettingsView's twin). diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift index 4adfc326..266f7500 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadChrome.swift @@ -5,6 +5,8 @@ // iOS/iPadOS, macOS (the couch Mac-mini case), and tvOS — where the same screens are driven by // the native focus engine instead of the controller poll (see GamepadCarousel/GamepadMenuList). +import Glur +import GlurBackdrop import PunktfunkKit import SwiftUI #if os(iOS) || os(macOS) || os(tvOS) @@ -280,6 +282,45 @@ func gamepadLegendBottomPadding( return max(-displayBottom, margin - displayBottom) } +/// The tray gradient blur, back — as a real progressive BACKDROP blur this time. +/// +/// GamepadTrayScrim did this with `.ultraThinMaterial`, and a material by definition lifts and +/// tints whatever it blurs: it read grey over the aurora, and washed with the palette's ground it +/// read coloured, which is why 2590238b deleted it. Glur's `GlurView` blurs the backdrop through +/// a gradient with NO material stage on top, so the rows soften as they slide under the pinned +/// title and legend and nothing carries a colour. It is the library's PRIVATE-API product +/// (`GlurBackdrop`) — the public `.glur()` modifier is a shader on a view's own content and +/// silently no-ops over platform-backed views like ScrollView, so it cannot reach a backdrop at +/// all. Hit testing is disabled inside GlurView; the band never eats a touch. +/// +/// Mounted exactly where the scrim was: `.background` of each form screen's safe-area tray. +struct GamepadTrayBlur: View { + let edge: VerticalEdge + + var body: some View { + // offset 0 puts the ramp's LITERAL ZERO exactly at the band's content edge, so nothing + // in the open field is touched — which is why, unlike the scrim, this band takes NO + // content-side overhang. The scrim's -44/-72 runway existed because a material carries + // body at every alpha and had to dissolve OUTSIDE the tray; carrying those numbers over + // here blurred fully-visible rows at rest (field verdict on the first cut). Full + // strength lands at 60% of the band, so the tray's own text always sits on the strong + // region while the ramp still reads as a gradient, not an edge. + GlurView( + radius: 14, offset: 0, interpolation: 0.6, + direction: edge == .top ? .up : .down) + // Full-bleed by LAYOUT, not `.ignoresSafeArea()`: safe-area expansion resolves a + // beat after insertion (outside any geometry group and outside this view's own + // transaction), which reads as a visible pop. 80 pt clears every inset on every + // device, and backgrounds never clip — the overhang simply draws. + .padding(edge == .top ? .top : .bottom, -80) + .padding(.horizontal, -80) + // And the shape must NEVER animate: mounted inside a pushed shell layer, any late + // geometry would ride the push's transaction and visibly grow into place. The + // layer's own fade/slide still carries the band; only its SHAPE is pinned. + .transaction { $0.animation = nil } + } +} + /// One glyph + label cell in a hint bar. struct GamepadHint: Identifiable { let glyph: String diff --git a/clients/apple/Sources/PunktfunkClient/Home/GamepadLibraryScreen.swift b/clients/apple/Sources/PunktfunkClient/Home/GamepadLibraryScreen.swift index 0cfcf87c..297bc487 100644 --- a/clients/apple/Sources/PunktfunkClient/Home/GamepadLibraryScreen.swift +++ b/clients/apple/Sources/PunktfunkClient/Home/GamepadLibraryScreen.swift @@ -38,6 +38,7 @@ struct GamepadLibraryScreen: View { .padding(.horizontal, 24) .padding(.top, gamepadTitleTopPadding(compact: compact)) .padding(.bottom, gamepadTitleBottomPadding(compact: compact)) + .background { GamepadTrayBlur(edge: .top) } } // A hardware keyboard's Esc still closes, without chrome. .background { diff --git a/clients/apple/Sources/PunktfunkClient/Settings/AcknowledgementsView.swift b/clients/apple/Sources/PunktfunkClient/Settings/AcknowledgementsView.swift index d9f693ea..85b5ab3a 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/AcknowledgementsView.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/AcknowledgementsView.swift @@ -54,6 +54,15 @@ struct AcknowledgementsView: View { Divider() + Text("Swift packages") + .font(.geist(Self.headlineFont, .semibold, relativeTo: .headline)) + Text("Punktfunk uses Glur (progressive backdrop blur), " + + "© 2023 João Gabriel, under the MIT License.") + .font(.geist(Self.captionFont, relativeTo: .caption)) + .foregroundStyle(.secondary) + + Divider() + Text("Third-party software") .font(.geist(Self.headlineFont, .semibold, relativeTo: .headline)) Text( diff --git a/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift b/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift index 3290d2cd..b3543630 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift @@ -163,6 +163,7 @@ struct GamepadSettingsView: View { } .padding(.top, gamepadTitleTopPadding(compact: compact)) .padding(.bottom, gamepadTitleBottomPadding(compact: compact)) + .background { GamepadTrayBlur(edge: .top) } } .safeAreaInset(edge: .bottom, alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 8) { @@ -182,6 +183,7 @@ struct GamepadSettingsView: View { compact ? 12 : 18, tier: metrics.tier, displayBottom: displayBottomInset)) .padding(.top, compact ? 6 : 10) .frame(maxWidth: .infinity, alignment: .leading) + .background { GamepadTrayBlur(edge: .bottom) } } // The launcher's living field, calmed (GamepadFormBackground) — the glass rows keep real // colour and luminance to lens without the launcher's contrast, and the palette setting diff --git a/clients/apple/Sources/PunktfunkClient/Trust/GamepadPairView.swift b/clients/apple/Sources/PunktfunkClient/Trust/GamepadPairView.swift index a2647f29..4d2054ee 100644 --- a/clients/apple/Sources/PunktfunkClient/Trust/GamepadPairView.swift +++ b/clients/apple/Sources/PunktfunkClient/Trust/GamepadPairView.swift @@ -76,6 +76,7 @@ struct GamepadPairView: View { .padding(.top, gamepadTitleTopPadding(compact: compact)) .padding(.bottom, gamepadTitleBottomPadding(compact: compact)) .frame(maxWidth: .infinity, alignment: .leading) + .background { GamepadTrayBlur(edge: .top) } } .safeAreaInset(edge: .bottom, spacing: 0) { bottomTray @@ -87,6 +88,7 @@ struct GamepadPairView: View { gamepadLegendBottomPadding( compact ? 12 : 18, tier: metrics.tier, displayBottom: displayBottomInset)) .padding(.top, compact ? 6 : 10) + .background { GamepadTrayBlur(edge: .bottom) } } // Hosted in the shell, the field is the shell's own (see GamepadAddHostView's twin). .background {