Files

163 lines
8.7 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import Combine
import GameController
struct ControllerInputState: Equatable {
var buttons: Set<String> = []
var lx: Float = 0, ly: Float = 0, rx: Float = 0, ry: Float = 0
var lt: Float = 0, rt: Float = 0
var playStation = false
var playStation5 = false
var active: Bool {
!buttons.isEmpty || [lx,ly,rx,ry].contains {abs($0)>0.15} || lt>0.1 || rt>0.1
}
init() {}
init(_ g:GCExtendedGamepad) {
let inputs: [(String,GCControllerButtonInput?)] = [
("a",g.buttonA),("b",g.buttonB),("x",g.buttonX),("y",g.buttonY),
("lb",g.leftShoulder),("rb",g.rightShoulder),("ls",g.leftThumbstickButton),("rs",g.rightThumbstickButton),
("menu",g.buttonMenu),("options",g.buttonOptions),
("up",g.dpad.up),("down",g.dpad.down),("left",g.dpad.left),("right",g.dpad.right)]
buttons=Set(inputs.compactMap {$0.1?.isPressed == true ? $0.0 : nil})
lx=g.leftThumbstick.xAxis.value;ly=g.leftThumbstick.yAxis.value
rx=g.rightThumbstick.xAxis.value;ry=g.rightThumbstick.yAxis.value
lt=g.leftTrigger.value;rt=g.rightTrigger.value
playStation5=g is GCDualSenseGamepad
playStation=playStation5 || g is GCDualShockGamepad
}
}
// Sample only while the tester is visible. No handlers, pairing or background capture.
@MainActor final class ControllerTester: ObservableObject {
@Published private(set) var devices: [DetectedController] = []
@Published var selected: ObjectIdentifier? {
didSet {if selected != oldValue {clear();confirmed=false}}
}
@Published private(set) var state=ControllerInputState()
@Published private(set) var confirmed=false
private let query: () -> [GCController]
init(query:(() -> [GCController])? = nil) {self.query=query ?? {GCController.controllers()};sample()}
func clear() {state=ControllerInputState()}
func sample() {
let connected=query().filter {$0.extendedGamepad != nil}
let next=connected.map {DetectedController(id:ObjectIdentifier($0),name:$0.vendorName ?? "",supported:true)}
if next.map(\.id) != devices.map(\.id) {devices=next}
if !next.contains(where:{$0.id == selected}) {selected=next.first?.id}
guard let controller=connected.first(where:{ObjectIdentifier($0)==selected}),let pad=controller.extendedGamepad else {
clear();confirmed=false;return
}
let current=ControllerInputState(pad)
if current != state {state=current}
if current.active && !confirmed {confirmed=true}
}
}
struct ControllerTestView: View {
let text: (String) -> String
var embedded=false
@Environment(\.dismiss) private var dismiss
@StateObject private var tester=ControllerTester()
private let lime=Color(red:0.79,green:0.95,blue:0.37)
private let tick=Timer.publish(every:1.0/60.0,on:.main,in:.common).autoconnect()
private var state: ControllerInputState {tester.state}
private func button(_ key:String,_ label:String,_ size:CGFloat=36) -> some View {
let pressed=state.buttons.contains(key)
return Text(label).font(.system(size:13,weight:.bold,design:.rounded)).lineLimit(1).minimumScaleFactor(0.7)
.frame(width:size,height:size).foregroundStyle(pressed ? Color.black : Color.white.opacity(0.75))
.background(pressed ? lime : Color.white.opacity(0.08),in:RoundedRectangle(cornerRadius:size/3))
.overlay(RoundedRectangle(cornerRadius:size/3).stroke(pressed ? lime : Color.white.opacity(0.12)))
.accessibilityLabel(label).accessibilityValue(pressed ? text("Presionado") : text("Suelto"))
}
private func stick(_ key:String,_ label:String,_ x:Float,_ y:Float) -> some View {
VStack(spacing:8) {
ZStack {
Circle().fill(Color.black.opacity(0.24)).frame(width:92,height:92)
Path {p in p.move(to:CGPoint(x:46,y:4));p.addLine(to:CGPoint(x:46,y:88));p.move(to:CGPoint(x:4,y:46));p.addLine(to:CGPoint(x:88,y:46))}
.stroke(Color.white.opacity(0.08),lineWidth:1).frame(width:92,height:92)
Circle().fill(state.buttons.contains(key) ? lime : Color.white.opacity(0.72)).frame(width:38,height:38)
.offset(x:CGFloat(x)*26,y:-CGFloat(y)*26)
}
Text(label).font(.system(size:11,weight:.semibold)).foregroundStyle(.secondary)
}.accessibilityElement(children:.ignore).accessibilityLabel(label)
.accessibilityValue(String(format:"X %.2f, Y %.2f",x,y))
}
private func trigger(_ label:String,_ value:Float) -> some View {
VStack(alignment:.leading,spacing:7) {
HStack {Text(label);Spacer();Text("\(Int(value*100))%").monospacedDigit()}
.font(.system(size:12,weight:.semibold)).foregroundStyle(value>0.1 ? lime : .secondary)
ProgressView(value:Double(value)).tint(lime)
}.frame(width:170)
}
var body: some View {
VStack(alignment:.leading,spacing:20) {
HStack {
VStack(alignment:.leading,spacing:5) {
Text(text("Prueba tu control")).font(.system(size:25,weight:.bold,design:.rounded))
Text(text("Pulsa botones y mueve los sticks. Verás cada movimiento aquí."))
.font(.system(size:13)).foregroundStyle(.secondary)
}
Spacer()
if !embedded {Button(text("Cerrar")){dismiss()}.keyboardShortcut(.cancelAction)}
}
if tester.devices.isEmpty {
Label(text("Conecta un control a este Mac por Bluetooth."),systemImage:"gamecontroller")
.font(.system(size:13)).foregroundStyle(.secondary)
} else {
HStack {
if tester.devices.count>1 {
Picker(text("Control"),selection:$tester.selected) {
ForEach(tester.devices) {device in
Text(device.name.isEmpty ? text("Control") : device.name).tag(Optional(device.id))
}
}.frame(maxWidth:310)
} else {
Text(tester.devices.first?.name.isEmpty == false ? tester.devices.first!.name : text("Control"))
.font(.system(size:14,weight:.semibold))
}
Spacer()
Label(text(tester.confirmed ? "¡Está respondiendo!" : "Esperando tu primera pulsación…"),
systemImage:tester.confirmed ? "checkmark.circle.fill" : "hand.tap")
.font(.system(size:12,weight:.medium)).foregroundStyle(tester.confirmed ? lime : .secondary)
}
}
VStack(spacing:22) {
HStack {
trigger(state.playStation ? "L2" : "LT",state.lt)
Spacer()
trigger(state.playStation ? "R2" : "RT",state.rt)
}
HStack {
button("lb",state.playStation ? "L1" : "LB",42)
Spacer()
button("options",state.playStation5 ? "Create" : state.playStation ? "Share" : "View",42)
button("menu",state.playStation ? "Options" : "Menu",42)
Spacer()
button("rb",state.playStation ? "R1" : "RB",42)
}
HStack(alignment:.center) {
stick("ls","L3",state.lx,state.ly)
Spacer()
VStack(spacing:3) {
button("up","↑")
HStack(spacing:3) {button("left","←");Color.clear.frame(width:36,height:36);button("right","→")}
button("down","↓")
}
Spacer()
VStack(spacing:3) {
button("y",state.playStation ? "△" : "Y")
HStack(spacing:3) {button("x",state.playStation ? "□" : "X");button("b",state.playStation ? "○" : "B")}
button("a",state.playStation ? "×" : "A")
}
Spacer()
stick("rs","R3",state.rx,state.ry)
}
}.padding(26).background(Color.white.opacity(0.045),in:RoundedRectangle(cornerRadius:24))
Text(text("Esta prueba comprueba la entrada del control en este Mac, no la compatibilidad de un juego."))
.font(.system(size:11)).foregroundStyle(.secondary)
}.padding(26).frame(maxWidth:embedded ? .infinity : nil).frame(width:embedded ? nil : 690).preferredColorScheme(.dark)
.onReceive(tick) {_ in if NSApplication.shared.isActive {tester.sample()} else {tester.clear()}}
.onDisappear {tester.clear()}
}
}