feat(client): opt-in "Rumble on this phone" mirrors pad-0 rumble onto the device

iOS + Android: a new opt-in setting mirrors controller 1's rumble onto the
device's own actuator (Apple RumbleRenderer Actuator.device / CoreHaptics,
Android deviceBodyVibrator), so a motor-less clip-on pad still gives haptic
feedback through the phone/tablet it's clamped to. Default off; wired through
the gamepad settings on both platforms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 16:32:19 +02:00
parent 6db91cbf40
commit d58524c899
12 changed files with 239 additions and 11 deletions
@@ -1,6 +1,9 @@
// SettingsView's shared sections each setting's Section is defined exactly once here and
// composed by the per-platform bodies in SettingsView.swift.
#if os(iOS)
import CoreHaptics
#endif
import PunktfunkKit
import SwiftUI
@@ -471,6 +474,12 @@ extension SettingsView {
Text(option.label).tag(option.tag)
}
}
#if os(iOS)
// iPhone only in practice: hidden where the device itself can't play haptics (iPad).
if CHHapticEngine.capabilitiesForHardware().supportsHaptics {
Toggle("Rumble on this iPhone", isOn: $rumbleOnDevice)
}
#endif
#if !os(tvOS)
Toggle("Gamepad-optimized browsing", isOn: $gamepadUIEnabled)
#endif
@@ -487,6 +496,11 @@ extension SettingsView {
// for its own footer and has no such toggle to describe.
VStack(alignment: .leading, spacing: 6) {
Text(Self.controllersFooter)
#if os(iOS)
if CHHapticEngine.capabilitiesForHardware().supportsHaptics {
Text(Self.deviceRumbleFooter)
}
#endif
#if !os(tvOS)
Text(Self.gamepadUIFooter)
#endif