a652a9f187
Realign the card to the physical controller face: trigger bars and bumper chips in the top corners, sticks side by side in the upper center (stick clicks light up their rings instead of separate chips), D-pad lower left, face buttons lower right, and the Guide button centered between the clusters flanked by Select and Start. Co-Authored-By: qwen3.8-27b@q3_k_xl: realigned card to Xbox layout
162 lines
4.8 KiB
Swift
162 lines
4.8 KiB
Swift
import SwiftUI
|
|
|
|
// Gamepad-style visualization of a controller input snapshot: D-pad and face
|
|
// buttons on top, stick pads and triggers below, mirroring the physical
|
|
// layout of an Xbox controller.
|
|
|
|
struct DPadView: View {
|
|
let controller: ControllerInput
|
|
|
|
var body: some View {
|
|
VStack(spacing: 3) {
|
|
cell(.dPadUp)
|
|
HStack(spacing: 3) {
|
|
cell(.dPadLeft)
|
|
RoundedRectangle(cornerRadius: 5)
|
|
.fill(Color.secondary.opacity(0.28))
|
|
.frame(width: 30, height: 26)
|
|
cell(.dPadRight)
|
|
}
|
|
cell(.dPadDown)
|
|
}
|
|
}
|
|
|
|
private func cell(_ button: XboxButton) -> some View {
|
|
let pressed = controller.isPressed(button)
|
|
return RoundedRectangle(cornerRadius: 5)
|
|
.fill(pressed ? Color.green : Color.secondary.opacity(0.18))
|
|
.frame(width: 30, height: 26)
|
|
.overlay(
|
|
Image(systemName: button.dPadSymbol ?? "")
|
|
.font(.system(size: 9))
|
|
.foregroundStyle(pressed ? .white : .secondary)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct FaceButtonsView: View {
|
|
let controller: ControllerInput
|
|
|
|
var body: some View {
|
|
VStack(spacing: 4) {
|
|
faceButton(.y)
|
|
HStack(spacing: 10) {
|
|
faceButton(.x)
|
|
faceButton(.b)
|
|
}
|
|
faceButton(.a)
|
|
}
|
|
}
|
|
|
|
private func faceButton(_ button: XboxButton) -> some View {
|
|
let pressed = controller.isPressed(button)
|
|
return Text(button.label)
|
|
.font(.callout.weight(.semibold))
|
|
.frame(width: 32, height: 32)
|
|
.background(Circle().fill(pressed ? Color.green : Color.secondary.opacity(0.18)))
|
|
.foregroundStyle(pressed ? .white : .primary)
|
|
}
|
|
}
|
|
|
|
struct StickPadView: View {
|
|
let label: String
|
|
let x: Int16
|
|
let y: Int16
|
|
// True while the stick is clicked in (LS/RS).
|
|
var pressed = false
|
|
|
|
private let diameter: CGFloat = 64
|
|
|
|
var body: some View {
|
|
VStack(spacing: 4) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(Color.secondary.opacity(0.15))
|
|
Circle()
|
|
.stroke(pressed ? Color.green : Color.secondary.opacity(0.35),
|
|
lineWidth: pressed ? 2.5 : 1)
|
|
.padding(4)
|
|
Circle()
|
|
.fill(Color.accentColor)
|
|
.frame(width: 12, height: 12)
|
|
.offset(x: offset.width, y: offset.height)
|
|
}
|
|
.frame(width: diameter, height: diameter)
|
|
Text(label)
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
// Map the signed 16-bit stick values onto the pad, keeping the dot inside
|
|
// the ring.
|
|
private var offset: CGSize {
|
|
let travel = diameter / 2 - 10
|
|
return CGSize(
|
|
width: CGFloat(Double(x) / 32768.0 * travel),
|
|
height: CGFloat(Double(y) / 32768.0 * travel)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct TriggerBarView: View {
|
|
let label: String
|
|
let value: UInt16
|
|
|
|
private let barWidth: CGFloat = 56
|
|
|
|
var body: some View {
|
|
VStack(spacing: 4) {
|
|
ZStack(alignment: .leading) {
|
|
Color.secondary.opacity(0.18)
|
|
Color.accentColor
|
|
.frame(width: barWidth * fraction, alignment: .leading)
|
|
}
|
|
.frame(width: barWidth, height: 6)
|
|
.clipShape(RoundedRectangle(cornerRadius: 3))
|
|
Text(label)
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
private var fraction: CGFloat {
|
|
min(max(CGFloat(Double(value) / 1023.0), 0), 1)
|
|
}
|
|
}
|
|
|
|
// The large Guide button, centered between the D-pad and face buttons.
|
|
struct GuideButtonView: View {
|
|
let pressed: Bool
|
|
|
|
var body: some View {
|
|
VStack(spacing: 4) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(pressed ? Color.green : Color.secondary.opacity(0.25))
|
|
Image(systemName: "gamecontroller.fill")
|
|
.font(.system(size: 14))
|
|
.foregroundStyle(pressed ? .white : .secondary)
|
|
}
|
|
.frame(width: 36, height: 36)
|
|
Text("Guide")
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ButtonChip: View {
|
|
let button: XboxButton
|
|
let pressed: Bool
|
|
|
|
var body: some View {
|
|
Text(button.label)
|
|
.font(.caption2.weight(.medium))
|
|
.frame(minWidth: 30, minHeight: 20)
|
|
.background(pressed ? Color.green : Color.secondary.opacity(0.15))
|
|
.foregroundStyle(pressed ? .white : .primary)
|
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
|
}
|
|
}
|