feat: redesign UI with gamepad-style input monitor

Split the single-file Swift app into model, store, and view files
and rework the layout. The dongle card shows a status pill with an
adaptive info grid; controllers render as cards in an adaptive grid
that fills wider windows; each card uses a gamepad-style
visualization (D-pad, face buttons, stick pads, triggers) instead of
flat indicator rows. The window is now resizable and content reflows
instead of staying a fixed 420pt column.

Co-Authored-By: qwen3.8-27b@q3_k_xl: redesigned app UI and structure
This commit is contained in:
portersky
2026-08-29 14:47:41 +02:00
parent dfe7e8b87d
commit 4931bf5c18
8 changed files with 579 additions and 305 deletions
+137
View File
@@ -0,0 +1,137 @@
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
private let diameter: CGFloat = 64
var body: some View {
VStack(spacing: 4) {
ZStack {
Circle()
.fill(Color.secondary.opacity(0.15))
Circle()
.stroke(Color.secondary.opacity(0.35), lineWidth: 1)
.padding(6)
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)
}
}
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))
}
}